Skip to content

Commit 451f50d

Browse files
committed
feat(pt_expt): include vesin in DeepEval auto graph-builder ladder
Training keeps CPU on dense (vesin loops frames). Inference auto now shares resolve_auto_graph_builder: CUDA nv→vesin→dense, CPU vesin→dense. Signed-off-by: shaurya2k06 <shaurya2k06@gmail.com>
1 parent cc689a7 commit 451f50d

5 files changed

Lines changed: 102 additions & 28 deletions

File tree

deepmd/pt_expt/infer/deep_eval.py

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,19 @@ class DeepEval(DeepEvalBackend):
192192
neighbor_graph_method : str, default: "auto"
193193
Carry-all graph builder for graph-form ``.pt2`` artifacts and
194194
graph-routed ``.pt`` checkpoints
195-
(``metadata["lower_input_kind"] == "graph"``): ``"auto"`` selects
196-
``"nv"`` on CUDA when nvalchemiops is available and otherwise falls
197-
back to ``"dense"``. ``"vesin"`` remains explicit opt-in because it
198-
loops over frames in Python. Explicit
195+
(``metadata["lower_input_kind"] == "graph"``): ``"auto"`` selects via
196+
:func:`~deepmd.pt_expt.utils.graph_builder.resolve_auto_graph_builder`
197+
(CUDA: ``nv`` if importable, else ``vesin`` if importable, else
198+
``dense``; CPU: ``vesin`` if importable, else ``dense``). Explicit
199199
``"dense"`` / ``"ase"`` / ``"vesin"`` / ``"nv"`` choices are preserved.
200200
A non-default value on any other artifact raises at construction because
201201
the knob would silently do nothing there; use ``nlist_backend`` for the
202202
nlist path instead. All builders emit the same neighbor set, so the
203-
choice is performance-only. Consolidating the two knobs into a single
204-
backend-selection API is deferred to the dense-nlist deprecation.
203+
choice is performance-only. Training keeps a separate CPU-dense auto
204+
policy (:func:`~deepmd.pt_expt.utils.graph_builder.resolve_neighbor_graph_method`)
205+
because vesin's per-frame Python loop is not a multi-frame training
206+
default. Consolidating the two knobs into a single backend-selection API
207+
is deferred to the dense-nlist deprecation.
205208
**kwargs : dict
206209
Keyword arguments.
207210
"""
@@ -281,23 +284,14 @@ def _resolve_neighbor_graph_method(method: str) -> str:
281284
if method != "auto":
282285
return method
283286

284-
from deepmd.pt.utils.nv_nlist import (
285-
is_nv_available,
286-
)
287287
from deepmd.pt_expt.utils.env import (
288288
DEVICE,
289289
)
290+
from deepmd.pt_expt.utils.graph_builder import (
291+
resolve_auto_graph_builder,
292+
)
290293

291-
if DEVICE.type == "cuda":
292-
if is_nv_available():
293-
return "nv"
294-
log.warning(
295-
"nvalchemi-toolkit-ops is unavailable; falling back from "
296-
"neighbor_graph_method='auto' to the dense graph builder. "
297-
"Install it with `pip install nvalchemi-toolkit-ops` to enable "
298-
"the NV graph builder."
299-
)
300-
return "dense"
294+
return resolve_auto_graph_builder(DEVICE)
301295

302296
def _setup_neighbor_backend(self, nlist_backend: str) -> None:
303297
"""Resolve the graph or neighbor-list construction strategy.

deepmd/pt_expt/utils/graph_builder.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,47 @@
1919
log = logging.getLogger(__name__)
2020

2121

22+
def resolve_auto_graph_builder(
23+
device: torch.device | str,
24+
) -> str:
25+
"""Resolve ``neighbor_graph_method="auto"`` to a concrete inference builder.
26+
27+
Single owner of the inference / DeepEval auto ladder (training uses
28+
:func:`resolve_neighbor_graph_method`, which keeps CPU on ``dense`` because
29+
vesin loops frames in Python and is not safe as a multi-frame training
30+
default):
31+
32+
* CUDA: ``nv`` if ``nvalchemiops`` is importable, else ``vesin`` if
33+
``vesin.torch`` is importable, else ``dense``.
34+
* CPU: ``vesin`` if ``vesin.torch`` is importable, else ``dense``.
35+
36+
``ase`` is never chosen automatically. All builders emit the same carry-all
37+
neighbor set; the choice is performance-only. Builders run eagerly outside
38+
traced / compiled regions, so this does not change ``.pt2`` artifacts.
39+
"""
40+
from deepmd.pt.utils.nv_nlist import (
41+
is_nv_available,
42+
)
43+
from deepmd.pt_expt.utils.vesin_neighbor_list import (
44+
is_vesin_torch_available,
45+
)
46+
47+
dev = torch.device(device)
48+
if dev.type == "cuda":
49+
if is_nv_available():
50+
return "nv"
51+
if is_vesin_torch_available():
52+
return "vesin"
53+
log.warning(
54+
"nvalchemi-toolkit-ops and vesin[torch] are unavailable; falling "
55+
"back from neighbor_graph_method='auto' to the dense graph builder."
56+
)
57+
return "dense"
58+
if is_vesin_torch_available():
59+
return "vesin"
60+
return "dense"
61+
62+
2263
def resolve_neighbor_graph_method(
2364
requested: str,
2465
device: torch.device,
@@ -36,6 +77,8 @@ def resolve_neighbor_graph_method(
3677
-------
3778
str
3879
The concrete builder name, either ``"dense"`` or ``"nv"``.
80+
Training auto never selects ``vesin`` (per-frame Python loop); use
81+
:func:`resolve_auto_graph_builder` for inference auto selection.
3982
4083
Raises
4184
------

deepmd/pt_expt/utils/vesin_graph_builder.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
99
Scope note: ``vesin.torch``'s API is single-system, so this builder LOOPS over
1010
frames in Python (~1 ms/frame call overhead measured on GPU). It is intended
11-
for ``nf == 1`` inference and CPU use. It is never on a default hot path:
12-
``neighbor_graph_method=None`` resolves to the ``"dense"`` converter, and
13-
vesin is explicit opt-in only. For batched multi-frame GPU work prefer
14-
``nv`` (:mod:`.nv_graph_builder`), which batches all frames in one kernel.
11+
for ``nf == 1`` inference and CPU use. Inference ``neighbor_graph_method="auto"``
12+
(:func:`~deepmd.pt_expt.utils.graph_builder.resolve_auto_graph_builder`) selects
13+
vesin only when ``vesin.torch`` is importable (CPU always; CUDA only when ``nv``
14+
is unavailable); otherwise it falls back to ``dense``. Training auto keeps CPU
15+
on ``dense`` and never selects vesin. Prefer ``nv`` (:mod:`.nv_graph_builder`)
16+
for batched multi-frame GPU work, which batches all frames in one kernel.
1517
"""
1618

1719
from __future__ import (

source/tests/pt_expt/infer/test_deep_eval_pt_checkpoint.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -423,15 +423,19 @@ class TestNeighborGraphMethodResolution(unittest.TestCase):
423423
"""Auto graph-builder selection must cover each host policy explicitly."""
424424

425425
def test_auto_resolution(self) -> None:
426+
# (device, nv, vesin, expected, warns)
426427
cases = (
427-
("cpu", False, "dense", False),
428-
("cuda", True, "nv", False),
429-
("cuda", False, "dense", True),
428+
("cpu", False, True, "vesin", False),
429+
("cpu", False, False, "dense", False),
430+
("cuda", True, True, "nv", False),
431+
("cuda", False, True, "vesin", False),
432+
("cuda", False, False, "dense", True),
430433
)
431-
for device_type, nv_available, expected, warns in cases:
434+
for device_type, nv_available, vesin_available, expected, warns in cases:
432435
with self.subTest(
433436
device_type=device_type,
434437
nv_available=nv_available,
438+
vesin_available=vesin_available,
435439
):
436440
with (
437441
mock.patch(
@@ -442,10 +446,14 @@ def test_auto_resolution(self) -> None:
442446
"deepmd.pt.utils.nv_nlist.is_nv_available",
443447
return_value=nv_available,
444448
),
449+
mock.patch(
450+
"deepmd.pt_expt.utils.vesin_neighbor_list.is_vesin_torch_available",
451+
return_value=vesin_available,
452+
),
445453
):
446454
if warns:
447455
with self.assertLogs(
448-
"deepmd.pt_expt.infer.deep_eval",
456+
"deepmd.pt_expt.utils.graph_builder",
449457
level="WARNING",
450458
):
451459
actual = PtExptDeepEval._resolve_neighbor_graph_method(

source/tests/pt_expt/model/test_graph_builder_dispatch.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,33 @@ def test_explicit_nv_rejects_cpu():
138138
resolve_neighbor_graph_method("nv", torch.device("cpu"))
139139

140140

141+
@pytest.mark.parametrize(
142+
("device", "nv", "vesin", "expected"),
143+
[
144+
("cpu", False, True, "vesin"),
145+
("cpu", True, False, "dense"),
146+
("cuda", True, True, "nv"),
147+
("cuda", False, True, "vesin"),
148+
("cuda", False, False, "dense"),
149+
],
150+
)
151+
def test_resolve_auto_graph_builder_ladder(
152+
device: str, nv: bool, vesin: bool, expected: str
153+
) -> None:
154+
from deepmd.pt_expt.utils.graph_builder import (
155+
resolve_auto_graph_builder,
156+
)
157+
158+
with (
159+
patch("deepmd.pt.utils.nv_nlist.is_nv_available", return_value=nv),
160+
patch(
161+
"deepmd.pt_expt.utils.vesin_neighbor_list.is_vesin_torch_available",
162+
return_value=vesin,
163+
),
164+
):
165+
assert resolve_auto_graph_builder(device) == expected
166+
167+
141168
@pytest.mark.skipif(not is_vesin_torch_available(), reason="vesin[torch] not installed")
142169
def test_vesin_matches_dense_energy_force():
143170
torch.manual_seed(0)

0 commit comments

Comments
 (0)