Skip to content

Commit 94f0ffe

Browse files
author
Han Wang
committed
make the full model exportable
1 parent 26a73cf commit 94f0ffe

3 files changed

Lines changed: 316 additions & 11 deletions

File tree

deepmd/pt/model/network/layernorm.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,9 @@ def forward(
8787
yy: torch.Tensor
8888
The output.
8989
"""
90-
# mean = xx.mean(dim=-1, keepdim=True)
91-
# variance = xx.var(dim=-1, unbiased=False, keepdim=True)
92-
# The following operation is the same as above, but will not raise error when using jit model to inference.
93-
# See https://github.com/pytorch/pytorch/issues/85792
9490
if xx.numel() > 0:
95-
variance, mean = torch.var_mean(xx, dim=-1, unbiased=False, keepdim=True)
91+
mean = xx.mean(dim=-1, keepdim=True)
92+
variance = xx.var(dim=-1, unbiased=False, keepdim=True)
9693
yy = (xx - mean) / torch.sqrt(variance + self.eps)
9794
else:
9895
yy = xx

deepmd/pt/utils/nlist.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,20 @@ def nlist_distinguish_types(
308308
snsel = tnlist.shape[2]
309309
for ii, ss in enumerate(sel):
310310
# nloc x s(nsel)
311-
# to int because bool cannot be sort on GPU
312-
pick_mask = (tnlist == ii).to(torch.int32)
313-
# nloc x s(nsel), stable sort, nearer neighbors first
314-
pick_mask, imap = torch.sort(pick_mask, dim=-1, descending=True, stable=True)
315-
# nloc x s(nsel)
311+
mask = tnlist == ii
312+
order = (
313+
snsel - torch.arange(snsel, device=mask.device, dtype=torch.int64)
314+
).view(1, 1, -1)
315+
key = torch.where(
316+
mask,
317+
order.to(torch.float32),
318+
torch.full_like(order, -1, dtype=torch.float32),
319+
)
320+
topk_vals, imap = torch.topk(key, ss, dim=-1, largest=True)
321+
# nloc x nsel[ii]
316322
inlist = torch.gather(nlist, 2, imap)
317-
inlist = inlist.masked_fill(~(pick_mask.to(torch.bool)), -1)
323+
valid = topk_vals > 0
324+
inlist = inlist.masked_fill(~valid, -1)
318325
# nloc x nsel[ii]
319326
ret_nlist.append(inlist[..., :ss])
320327
return torch.concat(ret_nlist, dim=-1)
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
import unittest
3+
from contextlib import (
4+
contextmanager,
5+
)
6+
7+
import torch
8+
import torch.export
9+
from executorch.exir import (
10+
EdgeCompileConfig,
11+
to_edge,
12+
)
13+
from executorch.runtime import (
14+
Runtime,
15+
)
16+
17+
from deepmd.pt.model.descriptor.dpa1 import (
18+
DescrptDPA1,
19+
)
20+
from deepmd.pt.model.descriptor.se_a import (
21+
DescrptSeA,
22+
)
23+
from deepmd.pt.model.model import (
24+
get_model,
25+
)
26+
from deepmd.pt.utils import (
27+
env,
28+
)
29+
from deepmd.pt.utils.nlist import (
30+
extend_input_and_build_neighbor_list,
31+
)
32+
33+
34+
@contextmanager
35+
def _cpu_default_device():
36+
prior_device = torch._C._get_default_device()
37+
torch.set_default_device("cpu")
38+
try:
39+
yield
40+
finally:
41+
torch.set_default_device(prior_device)
42+
43+
44+
class TestExecutorchConsistency(unittest.TestCase):
45+
def setUp(self):
46+
self.rcut = 6.0
47+
self.rcut_smth = 5.0
48+
self.sel = [4, 4]
49+
self.ntypes = 2
50+
self.neuron = [10, 10]
51+
self.axis_neuron = 4
52+
self.precision = "float32" # Executorch primarily supports float32
53+
self.dtype = torch.float32
54+
55+
def _test_descriptor_consistency(self, model, coord_ext, atype_ext, nlist):
56+
model.eval()
57+
58+
# 1. Run pristine model
59+
with torch.no_grad():
60+
expected_output = model(coord_ext, atype_ext, nlist)
61+
# The descriptor returns a tuple, usually the first element is the main descriptor
62+
if isinstance(expected_output, tuple):
63+
expected_output = expected_output[0]
64+
65+
# 2. Export and compile to Executorch
66+
try:
67+
exported_program = torch.export.export(model, (coord_ext, atype_ext, nlist))
68+
with _cpu_default_device():
69+
edge_program = to_edge(
70+
exported_program,
71+
compile_config=EdgeCompileConfig(
72+
_core_aten_ops_exception_list=[torch.ops.aten.sort.stable]
73+
),
74+
)
75+
executorch_program = edge_program.to_executorch()
76+
except Exception as e:
77+
self.fail(f"Executorch compilation failed for {type(model).__name__}: {e}")
78+
79+
# 3. Execute with Executorch Runtime
80+
program = Runtime.get().load_program(executorch_program.buffer)
81+
82+
# Prepare inputs for executorch
83+
# Executorch runtime inputs usually need to be flat list of tensors or similar depending on the API
84+
# The current python API for runtime might vary slightly by version.
85+
86+
# Using the low-level API style or the higher level if available.
87+
# Assuming `forward` method is the entry point (index 0 usually).
88+
89+
# Load the method (plan)
90+
method_name = "forward"
91+
try:
92+
# Prepare inputs
93+
# Note: Executorch runtime expects inputs to be compatible with the memory plan
94+
# Here we simply pass the tensors.
95+
# The execution model in python bindings typically takes a list of inputs.
96+
inputs = [coord_ext, atype_ext, nlist]
97+
98+
# Execute
99+
# Note: The specific API call might be `run`, `execute`, or similar.
100+
# Based on standard usage pattern:
101+
method = program.load_method(method_name)
102+
result = method.execute(inputs)
103+
104+
# The result is typically a list of outputs
105+
actual_output = result[0]
106+
107+
# 4. Compare results
108+
# Allow for some tolerance due to different backends/precisions if any
109+
torch.testing.assert_close(
110+
actual_output, expected_output, rtol=1e-4, atol=1e-4
111+
)
112+
113+
except Exception as e:
114+
self.fail(f"Executorch execution failed: {e}")
115+
116+
def test_se_e2_a_consistency(self):
117+
model = DescrptSeA(
118+
rcut=self.rcut,
119+
rcut_smth=self.rcut_smth,
120+
sel=self.sel,
121+
neuron=self.neuron,
122+
axis_neuron=self.axis_neuron,
123+
precision=self.precision,
124+
trainable=False,
125+
).to(env.DEVICE)
126+
127+
nf = 1
128+
nloc = 5
129+
coord = torch.randn(nf, nloc * 3, device=env.DEVICE, dtype=self.dtype)
130+
atype = torch.randint(
131+
0, self.ntypes, (nf, nloc), dtype=torch.int32, device=env.DEVICE
132+
)
133+
coord_ext, atype_ext, _, nlist = extend_input_and_build_neighbor_list(
134+
coord,
135+
atype,
136+
self.rcut,
137+
self.sel,
138+
mixed_types=model.mixed_types(),
139+
)
140+
coord_ext = coord_ext.view(nf, -1)
141+
142+
self._test_descriptor_consistency(model, coord_ext, atype_ext, nlist)
143+
144+
def test_dpa1_consistency(self):
145+
# Note: DPA1 failed compilation in previous turn due to var_mean.correction
146+
# We include it here. If it fails compilation, the helper returns early or we can catch it.
147+
# Ideally we fix the compilation or skip if known broken.
148+
model = DescrptDPA1(
149+
rcut=self.rcut,
150+
rcut_smth=self.rcut_smth,
151+
sel=self.sel,
152+
ntypes=self.ntypes,
153+
neuron=self.neuron,
154+
axis_neuron=self.axis_neuron,
155+
precision=self.precision,
156+
trainable=False,
157+
).to(env.DEVICE)
158+
159+
nf = 1
160+
nloc = 5
161+
coord = torch.randn(nf, nloc * 3, device=env.DEVICE, dtype=self.dtype)
162+
atype = torch.randint(
163+
0, self.ntypes, (nf, nloc), dtype=torch.int32, device=env.DEVICE
164+
)
165+
coord_ext, atype_ext, _, nlist = extend_input_and_build_neighbor_list(
166+
coord,
167+
atype,
168+
self.rcut,
169+
self.sel,
170+
mixed_types=model.mixed_types(),
171+
)
172+
coord_ext = coord_ext.view(nf, -1)
173+
174+
self._test_descriptor_consistency(model, coord_ext, atype_ext, nlist)
175+
176+
def test_full_model_consistency(self):
177+
# Full EnergyModel with se_e2_a descriptor
178+
model_params = {
179+
"type_map": ["O", "H"],
180+
"descriptor": {
181+
"type": "se_e2_a",
182+
"sel": self.sel,
183+
"rcut_smth": self.rcut_smth,
184+
"rcut": self.rcut,
185+
"neuron": self.neuron,
186+
"axis_neuron": self.axis_neuron,
187+
"precision": self.precision,
188+
},
189+
"fitting_net": {
190+
"type": "direct_force_ener",
191+
"neuron": [10, 10],
192+
"precision": self.precision,
193+
},
194+
}
195+
model = get_model(model_params).to(env.DEVICE)
196+
model.eval()
197+
198+
nf = 1
199+
nloc = 5
200+
coord = torch.randn(nf, nloc * 3, device=env.DEVICE, dtype=self.dtype)
201+
atype = torch.randint(
202+
0, self.ntypes, (nf, nloc), dtype=torch.int32, device=env.DEVICE
203+
)
204+
coord_ext, atype_ext, _, nlist = extend_input_and_build_neighbor_list(
205+
coord,
206+
atype,
207+
self.rcut,
208+
self.sel,
209+
mixed_types=model.mixed_types(),
210+
)
211+
coord_ext = coord_ext.view(nf, -1)
212+
213+
# 1. Run pristine model (forward_lower)
214+
# Note: forward_lower returns a dict. Executorch output will be a flat tuple of tensors (values of the dict usually, or based on graph return).
215+
# We need to wrap it to return specific tensor(s) or handle dict if export supports it (export usually flattens).
216+
217+
# Pristine output (dict)
218+
pristine_dict = model.forward_lower(coord_ext, atype_ext, nlist)
219+
# We'll focus on 'energy' and 'force' (if available) for consistency
220+
# force requires grad calculation which we fixed.
221+
222+
class ForwardLowerWrapper(torch.nn.Module):
223+
def __init__(self, model):
224+
super().__init__()
225+
self.model = model
226+
227+
def forward(self, extended_coord, extended_atype, nlist):
228+
ret = self.model.forward_lower(extended_coord, extended_atype, nlist)
229+
# Return tuple of values to match typical export behavior for dicts or flatten them
230+
# Usually we want energy and force
231+
# We can return the values of the dict. Torch export might sort them or we define order.
232+
# Let's return explicit keys we care about.
233+
return ret["energy"], ret["atom_energy"], ret["dforce"]
234+
235+
wrapper = ForwardLowerWrapper(model)
236+
237+
# Recalculate pristine with wrapper to be sure
238+
# Note: We cannot use torch.no_grad() here because the model calculates forces using autograd.grad,
239+
# which requires the computation graph of energy to be active.
240+
expected_energy, expected_atom_energy, expected_force = wrapper(
241+
coord_ext, atype_ext, nlist
242+
)
243+
244+
# 2. Export and compile
245+
try:
246+
# Torch export may lift tensor constants created inside the model into the
247+
# graph. Some are produced in fake mode and can trigger strict failures.
248+
# Relax this check for the full-model export to keep the consistency test
249+
# focused on executorch compatibility.
250+
with torch._export.config.patch(error_on_lifted_constant_tensors=False):
251+
exported_program = torch.export.export(
252+
wrapper, (coord_ext, atype_ext, nlist)
253+
)
254+
with _cpu_default_device():
255+
edge_program = to_edge(
256+
exported_program,
257+
compile_config=EdgeCompileConfig(
258+
_core_aten_ops_exception_list=[torch.ops.aten.sort.stable]
259+
),
260+
)
261+
executorch_program = edge_program.to_executorch()
262+
except Exception as e:
263+
self.fail(f"Full model compilation failed: {e}")
264+
265+
# 3. Execute
266+
program = Runtime.get().load_program(executorch_program.buffer)
267+
inputs = [coord_ext, atype_ext, nlist]
268+
269+
try:
270+
method = program.load_method("forward")
271+
result = method.execute(inputs)
272+
# Result should be list of [energy, atom_energy, dforce]
273+
actual_energy = result[0]
274+
actual_atom_energy = result[1]
275+
actual_force = result[2]
276+
277+
# 4. Compare
278+
torch.testing.assert_close(
279+
actual_energy,
280+
expected_energy,
281+
rtol=1e-4,
282+
atol=1e-4,
283+
msg="Energy mismatch",
284+
)
285+
torch.testing.assert_close(
286+
actual_atom_energy,
287+
expected_atom_energy,
288+
rtol=1e-4,
289+
atol=1e-4,
290+
msg="Atom energy mismatch",
291+
)
292+
torch.testing.assert_close(
293+
actual_force, expected_force, rtol=1e-4, atol=1e-4, msg="Force mismatch"
294+
)
295+
296+
except Exception as e:
297+
self.fail(f"Full model execution failed: {e}")
298+
299+
300+
if __name__ == "__main__":
301+
unittest.main()

0 commit comments

Comments
 (0)