Skip to content

Commit 6373f24

Browse files
committed
feat(pt-expt): convert backend use auto to choose graph lower automatically
1 parent 6383e3c commit 6373f24

4 files changed

Lines changed: 123 additions & 77 deletions

File tree

deepmd/entrypoints/convert_backend.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,23 @@ def convert_backend(
3636
inp_hook = inp_backend.serialize_hook
3737
out_hook = out_backend.deserialize_hook
3838
data = inp_hook(INPUT)
39-
# Forward atomic_virial to pt_expt deserialize_to_file if applicable;
40-
# warn and skip the flag for backends that don't accept it so that
41-
# scripts passing --atomic-virial indiscriminately don't break.
39+
# Pass the optional flags a backend's deserialize hook accepts, probed by
40+
# signature so scripts stay backend-agnostic. ``lower_kind="auto"`` lets the
41+
# pt_expt hook pick the deploy-optimal lowering (the graph lower with its
42+
# fused operator for a graph-eligible .pt2, the dense nlist lower otherwise);
43+
# ``do_atomic_virial`` forwards ``--atomic-virial``.
4244
import inspect
4345

4446
sig = inspect.signature(out_hook)
47+
hook_kwargs: dict[str, Any] = {}
48+
if "lower_kind" in sig.parameters:
49+
hook_kwargs["lower_kind"] = "auto"
4550
if "do_atomic_virial" in sig.parameters:
46-
out_hook(OUTPUT, data, do_atomic_virial=atomic_virial)
47-
else:
48-
if atomic_virial:
49-
log.warning(
50-
"--atomic-virial is only meaningful for pt_expt .pt2/.pte "
51-
"outputs; ignoring it for output backend %s",
52-
out_backend.name,
53-
)
54-
out_hook(OUTPUT, data)
51+
hook_kwargs["do_atomic_virial"] = atomic_virial
52+
elif atomic_virial:
53+
log.warning(
54+
"--atomic-virial is only meaningful for pt_expt .pt2/.pte "
55+
"outputs; ignoring it for output backend %s",
56+
out_backend.name,
57+
)
58+
out_hook(OUTPUT, data, **hook_kwargs)

deepmd/kernels/cuda/dpa1/graph_compress.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,19 @@ def ensure_registered() -> None:
789789
_cpu_library.impl("dpa1_graph_compress", _cpu_forward, "CPU")
790790
_cpu_library.impl("dpa1_graph_compress_backward", _cpu_backward, "CPU")
791791
if ef_op_available():
792+
# The fused energy-force CPU reference (_ef_cpu, used for the freeze
793+
# pipeline's trace-time sample evaluation) dispatches through the fitting
794+
# and force / virial sub-operators, so their CPU implementations must be
795+
# registered too.
796+
from deepmd.kernels.cuda.edge_force_virial import (
797+
ensure_registered as ensure_force_registered,
798+
)
799+
from deepmd.kernels.cuda.graph_fitting import (
800+
ensure_registered as ensure_fitting_registered,
801+
)
802+
803+
ensure_fitting_registered()
804+
ensure_force_registered()
792805
torch.library.register_fake("deepmd::dpa1_graph_compress_energy_force")(
793806
_ef_fake
794807
)

deepmd/pt_expt/entrypoints/compress.py

Lines changed: 12 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
# SPDX-License-Identifier: LGPL-3.0-or-later
22
"""Compress a pt_expt model (.pte) by tabulating embedding nets."""
33

4-
import contextlib
54
import logging
6-
import os
7-
from collections.abc import (
8-
Iterator,
9-
)
105

116
from deepmd.pt_expt.utils.serialization import (
127
deserialize_to_file,
@@ -115,37 +110,25 @@ def enable_compression(
115110
#
116111
# A geometrically compressed graph-lower descriptor (DPA1 / se_atten strip,
117112
# attn_layer == 0) evaluates its tabulated embedding through fused CUDA
118-
# operators, so its compressed graph exports directly and the ``.pt2`` runs
119-
# the table kernel at inference. The trace forces ``DP_CUDA_INFER >= 2`` so
120-
# the end-to-end ``deepmd::dpa1_graph_compress_energy_force`` operator is
121-
# used: it returns the force as a value and stays opaque through
122-
# ``torch.export``, whereas the level-1 autograd lower is decomposed to aten
123-
# (the tabulated kernel is lost). Level 2 degrades to the level-1 fused table
124-
# operator when the mega operator is unavailable or the descriptor width is
125-
# not an instantiated kernel width; level 0 (the untraceable reference
126-
# tabulation) is never used here.
127-
#
128-
# Every other compressed descriptor keeps tabulated operators that make_fx
129-
# cannot trace: those export the UNCOMPRESSED graph and carry the compressed
130-
# dict in ``model.json`` so ``deserialize()`` restores the compression state
131-
# for the Python inference path.
113+
# operators that make_fx can trace, so its compressed graph exports directly
114+
# to a graph-lower ``.pt2``: ``deserialize_to_file`` bakes the end-to-end
115+
# fused table operator and the mandatory per-atom virial (see its docstring).
116+
# Every other compressed descriptor keeps tabulated operators make_fx cannot
117+
# trace: those export the UNCOMPRESSED graph and carry the compressed dict in
118+
# ``model.json`` so ``deserialize()`` restores the compression state for the
119+
# Python inference path.
132120
from deepmd.pt_expt.train.training import (
133121
_model_uses_graph_lower,
134122
)
135123

136124
model_def_script = model_dict.get("model_def_script")
137125
if output.endswith(".pt2") and _model_uses_graph_lower(model):
138126
log.info("Re-exporting compressed graph...")
139-
with _cuda_infer_at_least_2():
140-
deserialize_to_file(
141-
output,
142-
{"model": compressed_model_dict, "model_def_script": model_def_script},
143-
lower_kind="graph",
144-
# The per-atom virial is required by consumers that always read
145-
# it (the Kokkos ``pair_style deepmd/kk``); match the uncompressed
146-
# graph freeze, which also exports it.
147-
do_atomic_virial=True,
148-
)
127+
deserialize_to_file(
128+
output,
129+
{"model": compressed_model_dict, "model_def_script": model_def_script},
130+
lower_kind="graph",
131+
)
149132
else:
150133
log.info("Re-exporting compressed model...")
151134
deserialize_to_file(
@@ -158,29 +141,3 @@ def enable_compression(
158141
},
159142
)
160143
log.info("Compressed model saved to %s", output)
161-
162-
163-
@contextlib.contextmanager
164-
def _cuda_infer_at_least_2() -> Iterator[None]:
165-
"""Pin ``DP_CUDA_INFER`` to at least 2 for the duration of a trace.
166-
167-
Level 2 selects the end-to-end fused table operator, which stays opaque
168-
through ``torch.export``; the level-1 autograd lower is decomposed to aten
169-
(losing the tabulated kernel), and level 0 selects the untraceable
170-
reference tabulation. Level 2 degrades to level 1 internally when the mega
171-
operator is unavailable or ineligible, so it is a safe floor.
172-
"""
173-
from deepmd.kernels.utils import (
174-
cuda_infer_level,
175-
)
176-
177-
saved = os.environ.get("DP_CUDA_INFER")
178-
if cuda_infer_level() < 2:
179-
os.environ["DP_CUDA_INFER"] = "2"
180-
try:
181-
yield
182-
finally:
183-
if saved is None:
184-
os.environ.pop("DP_CUDA_INFER", None)
185-
else:
186-
os.environ["DP_CUDA_INFER"] = saved

deepmd/pt_expt/utils/serialization.py

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
import contextlib
23
import ctypes
34
import json
45
import logging
6+
import os
7+
from collections.abc import (
8+
Iterator,
9+
)
510
from typing import (
611
Any,
712
)
@@ -807,6 +812,56 @@ def _serialize_from_file_pt2(model_file: str) -> dict:
807812
return model_dict
808813

809814

815+
@contextlib.contextmanager
816+
def _cuda_infer_at_least_2() -> Iterator[None]:
817+
"""Pin ``DP_CUDA_INFER`` to at least 2 for the duration of a trace.
818+
819+
Level 2 bakes the end-to-end fused operator, which stays opaque through
820+
``torch.export``; the level-1 autograd lower is decomposed to aten (losing
821+
the hand-tuned kernel), and level 0 selects the untraceable reference
822+
tabulation. Level 2 degrades to level 1 internally when the fused operator
823+
is unavailable or ineligible, so it is a safe floor for any graph export.
824+
"""
825+
from deepmd.kernels.utils import (
826+
cuda_infer_level,
827+
)
828+
829+
saved = os.environ.get("DP_CUDA_INFER")
830+
if cuda_infer_level() < 2:
831+
os.environ["DP_CUDA_INFER"] = "2"
832+
try:
833+
yield
834+
finally:
835+
if saved is None:
836+
os.environ.pop("DP_CUDA_INFER", None)
837+
else:
838+
os.environ["DP_CUDA_INFER"] = saved
839+
840+
841+
def _resolve_lower_kind(model_file: str, data: dict, lower_kind: str) -> str:
842+
"""Resolve ``lower_kind="auto"`` to a concrete lower-forward schema.
843+
844+
``"auto"`` selects the graph lower for a graph-lower-eligible model exported
845+
to ``.pt2`` -- the deploy-optimal path, whose fused mega operator the graph
846+
export bakes -- and the dense nlist lower for everything else (a ``.pte``
847+
target, a spin model, or a descriptor without a graph lower). An explicit
848+
``"nlist"`` / ``"graph"`` is returned unchanged.
849+
"""
850+
if lower_kind != "auto":
851+
return lower_kind
852+
if not model_file.endswith(".pt2") or data["model"].get("type") == "spin_ener":
853+
return "nlist"
854+
from deepmd.pt_expt.model.model import (
855+
BaseModel,
856+
)
857+
from deepmd.pt_expt.train.training import (
858+
_model_uses_graph_lower,
859+
)
860+
861+
model = BaseModel.deserialize(data["model"])
862+
return "graph" if _model_uses_graph_lower(model) else "nlist"
863+
864+
810865
def deserialize_to_file(
811866
model_file: str,
812867
data: dict,
@@ -835,25 +890,42 @@ def deserialize_to_file(
835890
tracing the uncompressed model (make_fx cannot trace custom ops).
836891
do_atomic_virial : bool
837892
If True, export with per-atom virial correction (3 extra backward
838-
passes, ~2.5x slower). Default False for best performance.
893+
passes, ~2.5x slower). Default False for best performance. Forced True
894+
for a graph lower, whose LAMMPS Kokkos consumer always reads it.
839895
lower_kind : str
840896
Which lower-forward schema the compiled AOTI graph consumes:
841897
``"nlist"`` (default) traces the dense quartet
842898
(``extended_coord``/``extended_atype``/``nlist``/``mapping``);
843899
``"graph"`` traces the NeighborGraph schema
844900
(``atype``/``n_node``/``edge_index``/``edge_vec``/``edge_mask``) with a
845901
DYNAMIC edge axis ``E`` (``Dim("nedge", min=2)``), so the artifact
846-
accepts any system size. The selected schema is recorded as
847-
``lower_input_kind`` in ``metadata.json``.
902+
accepts any system size. ``"auto"`` (used by ``convert-backend``)
903+
resolves to ``"graph"`` for a graph-eligible ``.pt2`` and ``"nlist"``
904+
otherwise (see :func:`_resolve_lower_kind`). A graph lower always bakes
905+
the fused mega operator (``DP_CUDA_INFER >= 2``) and the per-atom virial.
906+
The selected schema is recorded as ``lower_input_kind`` in
907+
``metadata.json``.
848908
"""
849-
if model_file.endswith(".pt2"):
850-
_deserialize_to_file_pt2(
851-
model_file, data, model_json_override, do_atomic_virial, lower_kind
852-
)
909+
lower_kind = _resolve_lower_kind(model_file, data, lower_kind)
910+
# A graph lower deploys the fused mega operator: the trace must run at
911+
# DP_CUDA_INFER >= 2 (level 1 decomposes to aten under torch.export), and the
912+
# per-atom virial is mandatory for its LAMMPS Kokkos consumer. Enforcing both
913+
# here gives every producer -- freeze, compress, convert-backend -- the same
914+
# deployable artifact.
915+
if lower_kind == "graph":
916+
do_atomic_virial = True
917+
ctx: contextlib.AbstractContextManager = _cuda_infer_at_least_2()
853918
else:
854-
_deserialize_to_file_pte(
855-
model_file, data, model_json_override, do_atomic_virial, lower_kind
856-
)
919+
ctx = contextlib.nullcontext()
920+
with ctx:
921+
if model_file.endswith(".pt2"):
922+
_deserialize_to_file_pt2(
923+
model_file, data, model_json_override, do_atomic_virial, lower_kind
924+
)
925+
else:
926+
_deserialize_to_file_pte(
927+
model_file, data, model_json_override, do_atomic_virial, lower_kind
928+
)
857929

858930

859931
def _trace_and_export(

0 commit comments

Comments
 (0)