Skip to content

Commit 2aee81c

Browse files
authored
perf(dpa1): optimize graph CUDA inference and deployment (#5758)
DPA1 graph inference previously decomposed descriptor, fitting, and force/virial work into many generic tensor operations, retained large edge-scale autograd state, and lacked a shared sparse topology contract across Python export, the C++ runtime, and LAMMPS. Build an inference-oriented graph pipeline that keeps eligible attention-free DPA1 models and their neighbor data on the GPU while retaining explicit fallbacks for unsupported configurations. - Add inference-only CUDA operators for attention-free concat and strip DPA1. The uncompressed path fuses environment construction, the three-layer embedding network, type handling, moment reduction, Gram contraction, and analytical edge-gradient backward. The geometrically compressed strip path fuses quintic table interpolation, type-pair gating, moments, descriptors, and analytical gradients. It supports masked edges, canonical or permutation CSR, int32 or int64 addressing, width buckets from 8 through 256, non-bucket padding and slicing, and axis widths up to 16. - Add descriptor-agnostic fused energy fitting and force/virial operators. Run eligible fitting networks through pedantic FP32 cuBLAS GEMMs with TF32 disabled and fused bias, activation, timestep, residual, and derivative epilogues, while retaining atomic and per-frame energies in FP64. Reduce both edge incidences with one warp per node and no global floating-point atomics, then accumulate frame virials through FP64 partial reductions without materializing an (E, 9) outer-product tensor. - Define cumulative DP_CUDA_INFER levels. Level 1 uses separately registered descriptor, fitting, and CSR force operators with first-order autograd. Level 2 returns force and virial as values, using one opaque end-to-end operator for eligible uncompressed DPA1 and an explicit custom-operator chain for compressed DPA1. This removes the inference autograd tape, suppresses the unused rotation output, avoids retaining the descriptor only for shape metadata, and shortens saved-state lifetimes for memory reuse. - Add make_fx- and export-composable Triton kernels for the PyTorch se-family environment matrix and the dense and graph DPA1 environment convolutions. Their closed-form first backward preserves the force path while avoiding decomposed gather, switch, normalization, gating, and segment reduction chains. DP_TRITON_INFER=2 resolves per-GPU launch tables and freeze-time tuning fills uncovered model shapes; level 3 optionally enables the compensated FP16x3 final embedding GEMM. The default CUDA graph path remains FP32. - Select compressed-kernel resource policy at first uncaptured use from balanced or occupancy-oriented 128/256-thread launches, cache the result by device, direction, descriptor shape, topology, index width, and workload class, and retain architecture-specific fallbacks for small inputs, capture, or tuning failures. Build the native operators only with a CUDA-enabled PyTorch and include a lowest-supported portable PTX target alongside native code. - Extend NeighborGraph with keyword-only destination/source CSR orders, int64 row pointers, and an explicit destination_sorted property without changing its original positional constructor. Generic builders preserve payload order and omit CSR by default; consumers opt into CSR or stable destination-major canonicalization, which moves masked guards to the suffix and makes destination order the identity. Edge masks remain authoritative inside every row, and export validates permutations, row membership, bounds, and canonical identity before tracing. - Make graph-form AOTInductor deployment select the optimized path automatically for eligible .pt2 conversion and export compressed graph models directly. Keep frame, node, and edge axes dynamic; preserve custom operators through export; record the graph edge-vector dtype in metadata; and make per-atom virial part of the graph artifact contract required by the Kokkos consumer. Eligible compressed artifacts accept FP32 edge geometry directly, while generic and uncompressed graph artifacts retain the FP64 geometry ABI. Use int64 Inductor indexing and bounded Triton launch tiling for large dynamic graphs. - Extend DeepEval and the C++ API to consume the same canonical graph ABI. Add device-edge capability and dtype queries, FP32 and FP64 edge-vector overloads, runtime frame and atomic parameters, total-versus-owned node counts, and optional communication metadata. Host ingestion canonicalizes arbitrary payloads, while the device path constructs destination identity CSR and source order with histogram, prefix-sum, and counting scatter. - Add pair_style deepmd/kk for device-resident edge and graph .pt2 inference. Build compact model-cutoff edges from the Kokkos full neighbor list with count, scan, and fill passes; compact NULL-mapped atom types; emit FP32 or FP64 vectors according to artifact metadata; and scatter model-node outputs back on device. Single-rank execution folds periodic images onto local owners, whereas domain decomposition retains local-plus-halo nodes and explicitly reverse-communicates force and centroid per-atom virial through either device-aware or host-staged communication. Message-passing edge artifacts use their with-comm forward, including empty-rank phantom inputs; message-passing graph artifacts fail fast because that multi-rank ABI is not supported. - Preserve graph correctness at ownership and masking boundaries. Exclude halo fitting outputs from energy, zero halo atomic parameters, retain virtual-atom and pair-exclusion masks in fused level 2, handle zero-node graphs, and reduce frame virial from node virials instead of a contended edge scatter. Validate and broadcast multi-frame fparam/aparam layouts, fix wide atomic-parameter allocation when filtering virtual atoms, and avoid copying incompletely constructed LAMMPS model members. Reduce graph-builder overhead with bounded GPU neighbor-capacity estimates and direct length-three periodic-shift reductions. - Require regeneration of graph-form .pt2 artifacts because their positional ABI now includes n_local and both CSR views. Compressed graph export is limited to FP32, geometrically compressed, attention-free strip models without excluded type pairs, with output width at most 256 and axis_neuron <= 16. The uncompressed CUDA descriptor requires three FP32 embedding layers whose widths stay equal or double within its compiled bounds. Other configurations continue through Triton, reference graph, or nlist lowering. deepmd/kk additionally requires a GPU edge/graph artifact, one model, and a valid atom map. - Add regression coverage for CUDA and Triton forward/gradient parity, compressed and uncompressed end-to-end energy, force, global virial, atomic energy, and atom virial; smooth and non-smooth one- and two-sided type gates; residual and activation variants; non-power-of-two widths; int32/int64 addressing; masked cached edges; canonical and permuted CSR; ownership; zero-node and many-frame reductions; make_fx, torch.compile, and dynamic graph export. Extend DeepEval and C++ tests for dynamic edge counts, multi-rank graph ingestion, parameter validation and broadcasting, atomic outputs, CSR construction, and virtual-atom parameter preservation.
1 parent 3122138 commit 2aee81c

112 files changed

Lines changed: 22806 additions & 1029 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

deepmd/dpmodel/atomic_model/base_atomic_model.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,51 @@ def forward_common_atomic(
320320
atom_mask = xp_take_first_n(ext_atom_mask, 1, nloc)
321321
return self._finalize_atomic_ret(ret_dict, atom_mask, atype)
322322

323+
def _prepare_graph_nodes(
324+
self,
325+
n_node: Array,
326+
n_local: Array | None,
327+
atype: Array,
328+
reference: Array,
329+
) -> tuple[Array, Array]:
330+
"""Apply node masks shared by generic and compact graph forwards."""
331+
xp = array_api_compat.array_namespace(reference)
332+
atype = xp.asarray(atype, device=array_api_compat.device(reference))
333+
atom_mask = self.make_atom_mask(atype)
334+
atype_clamped = xp.where(atom_mask, atype, xp.zeros_like(atype))
335+
output_mask = atom_mask
336+
if n_local is not None:
337+
from deepmd.dpmodel.utils.neighbor_graph import (
338+
node_ownership_mask,
339+
)
340+
341+
output_mask = output_mask & node_ownership_mask(
342+
n_node,
343+
n_local,
344+
atype.shape[0],
345+
)
346+
if self.atom_excl is not None:
347+
output_mask = xp.logical_and(
348+
output_mask,
349+
self.atom_excl.build_type_exclude_mask(atype_clamped),
350+
)
351+
return atype_clamped, output_mask
352+
353+
def _prepare_graph_inputs(
354+
self,
355+
graph: "NeighborGraph",
356+
atype: Array,
357+
) -> tuple["NeighborGraph", Array, Array]:
358+
"""Apply graph masks shared by standard and fused atomic forwards."""
359+
atype_clamped, output_mask = self._prepare_graph_nodes(
360+
graph.n_node,
361+
graph.n_local,
362+
atype,
363+
graph.edge_vec,
364+
)
365+
self._assert_graph_pair_excluded(graph, atype_clamped)
366+
return graph, atype_clamped, output_mask
367+
323368
def forward_common_atomic_graph(
324369
self,
325370
graph: "NeighborGraph",
@@ -359,25 +404,15 @@ def forward_common_atomic_graph(
359404
the result dict on the flat node axis, defined by the `FittingOutputDef`.
360405
361406
"""
362-
xp = array_api_compat.array_namespace(graph.edge_vec)
363-
atype = xp.asarray(atype, device=array_api_compat.device(graph.edge_vec))
364-
atom_mask = self.make_atom_mask(atype) # (N,) bool
365-
atype_clamped = xp.where(atom_mask, atype, xp.zeros_like(atype))
366-
# NOTE: model-level ``pair_exclude_types`` is NOT applied here. It is a
367-
# graph-BUILD transform (decision #18) already folded into
368-
# ``graph.edge_mask`` by the NeighborGraph builder (Python) or
369-
# ``applyPairExclusion`` (C++); this method consumes a pre-excluded graph.
370-
# Fail-safe (eager only): guard against a caller that skipped the build
371-
# seam, which would silently INCLUDE excluded pairs (fail-open).
372-
self._assert_graph_pair_excluded(graph, atype_clamped)
407+
graph, atype_clamped, output_mask = self._prepare_graph_inputs(graph, atype)
373408
ret_dict = self.forward_atomic_graph(
374409
graph,
375410
atype_clamped,
376411
fparam=fparam,
377412
aparam=aparam,
378413
charge_spin=charge_spin,
379414
)
380-
return self._finalize_atomic_ret(ret_dict, atom_mask, atype)
415+
return self._finalize_atomic_ret(ret_dict, output_mask, atype)
381416

382417
def _assert_nlist_pair_excluded(self, nlist: Array, extended_atype: Array) -> None:
383418
"""Fail-safe: assert the nlist reaching the dense seam is pre-excluded.
@@ -491,10 +526,16 @@ def _finalize_atomic_ret(
491526
492527
"""
493528
xp = array_api_compat.array_namespace(atype)
494-
ret_dict = self.apply_out_stat(ret_dict, atype)
529+
safe_atype = xp.where(
530+
self.make_atom_mask(atype),
531+
atype,
532+
xp.zeros_like(atype),
533+
)
534+
ret_dict = self.apply_out_stat(ret_dict, safe_atype)
495535
if self.atom_excl is not None:
496536
atom_mask = xp.logical_and(
497-
atom_mask, self.atom_excl.build_type_exclude_mask(atype)
537+
atom_mask,
538+
self.atom_excl.build_type_exclude_mask(safe_atype),
498539
)
499540
lead = atom_mask.shape # (nf, nloc) dense | (N,) graph
500541
for kk in ret_dict.keys():

deepmd/dpmodel/atomic_model/dp_atomic_model.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,46 @@
3737
)
3838

3939

40+
def _extend_graph_aparam(
41+
aparam: Array,
42+
n_node: Array,
43+
n_local: Array,
44+
n_total: int,
45+
) -> Array:
46+
"""Expand frame-local atomic parameters onto a local-plus-halo node axis."""
47+
import array_api_compat
48+
49+
from deepmd.dpmodel.utils.neighbor_graph import (
50+
frame_id_from_n_node,
51+
node_ownership_mask,
52+
)
53+
54+
xp = array_api_compat.array_namespace(aparam, n_node, n_local)
55+
frame_id = frame_id_from_n_node(n_node, n_total=n_total)
56+
frame_end = xp.cumulative_sum(n_node)
57+
frame_start = frame_end - n_node
58+
node_index = xp.arange(
59+
n_total,
60+
dtype=n_node.dtype,
61+
device=array_api_compat.device(n_node),
62+
)
63+
index_in_frame = node_index - xp.take(frame_start, frame_id, axis=0)
64+
local_capacity = aparam.shape[1]
65+
sentinel = xp.zeros(
66+
(aparam.shape[0], 1, aparam.shape[2]),
67+
dtype=aparam.dtype,
68+
device=array_api_compat.device(aparam),
69+
)
70+
padded_aparam = xp.concat([aparam, sentinel], axis=1)
71+
padded_capacity = local_capacity + 1
72+
local_index = index_in_frame % padded_capacity
73+
flat_index = frame_id * padded_capacity + local_index
74+
flat_aparam = xp.reshape(padded_aparam, (-1, aparam.shape[-1]))
75+
gathered = xp.take(flat_aparam, flat_index, axis=0)
76+
ownership = node_ownership_mask(n_node, n_local, n_total)
77+
return xp.where(ownership[:, None], gathered, xp.zeros_like(gathered))
78+
79+
4080
@BaseAtomicModel.register("standard")
4181
class DPAtomicModel(BaseAtomicModel):
4282
r"""Model give atomic prediction of some physical property.
@@ -302,10 +342,27 @@ def forward_atomic_graph(
302342
)
303343
fparam_node = None
304344
if fparam is not None:
305-
frame_id = frame_id_from_n_node(graph.n_node)
345+
frame_id = frame_id_from_n_node(
346+
graph.n_node,
347+
n_total=atype.shape[0],
348+
)
306349
fparam_node = xp.take(fparam, frame_id, axis=0) # (N, ndf)
350+
aparam_node = aparam
351+
if aparam is not None and graph.n_local is not None and aparam.ndim == 3:
352+
aparam_node = _extend_graph_aparam(
353+
aparam,
354+
graph.n_node,
355+
graph.n_local,
356+
atype.shape[0],
357+
)
307358
return self.fitting_net.call_graph(
308-
gg, atype, gr=rot_mat, g2=None, h2=None, fparam=fparam_node, aparam=aparam
359+
gg,
360+
atype,
361+
gr=rot_mat,
362+
g2=None,
363+
h2=None,
364+
fparam=fparam_node,
365+
aparam=aparam_node,
309366
)
310367

311368
def compute_or_load_stat(

deepmd/dpmodel/descriptor/dpa1.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -434,12 +434,13 @@ def uses_graph_lower(self) -> bool:
434434
"""Returns whether this descriptor supports the graph-native lower.
435435
436436
The graph-native lower (``call_graph``) covers the factorizable path
437-
AND transformer attention (``attn_layer >= 0``, NeighborGraph PR-D)
438-
with concat OR strip type-embedding. ``exclude_types`` is fully
439-
supported via
437+
and transformer attention (``attn_layer >= 0``) with concat or strip
438+
type embedding. ``exclude_types`` is applied through
440439
:func:`~deepmd.dpmodel.utils.neighbor_graph.apply_pair_exclusion`.
441-
Compressed descriptors are the remaining ineligible config and fall
442-
back to the legacy dense path, so those models keep working unchanged.
440+
Geo-compressed strip models
441+
(``geo_compress=True``, ``attn_layer == 0``) are also graph-eligible;
442+
tebd-only compression and compressed descriptors with descriptor-level
443+
exclusions stay on the legacy dense path.
443444
444445
Eligibility does NOT imply numerical interchangeability with the
445446
dense route for every config: with ``smooth_type_embedding=True``
@@ -449,13 +450,13 @@ def uses_graph_lower(self) -> bool:
449450
"""
450451
if self._graph_lower_disabled:
451452
return False
452-
# compressed descriptors have no graph kernel (geo/tebd tabulation is
453-
# dense-only); keep them on the legacy dense path.
454453
if self.compress:
455-
return False
456-
# strip is graph-eligible (per-edge factorized embedding, no neighbor
457-
# coupling); exclude_types is graph-native via ``apply_pair_exclusion``
458-
# (owned at this seam). Only compression / the disable flag force dense.
454+
return (
455+
self.geo_compress
456+
and self.se_atten.tebd_input_mode == "strip"
457+
and self.se_atten.attn_layer == 0
458+
and not self.se_atten.exclude_types
459+
)
459460
return self.se_atten.tebd_input_mode in ("concat", "strip")
460461

461462
def disable_graph_lower(self) -> None:
@@ -1775,7 +1776,7 @@ def call_graph(
17751776
Notes
17761777
-----
17771778
Known limitations:
1778-
- ``tebd_input_mode`` in {"concat", "strip"}; compressed descriptors stay dense;
1779+
- ``tebd_input_mode`` in {"concat", "strip"};
17791780
- ``exclude_types`` is applied graph-natively via ``apply_pair_exclusion``.
17801781
"""
17811782
from deepmd.dpmodel.utils.neighbor_graph import (

deepmd/dpmodel/model/make_model.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -666,28 +666,25 @@ def forward_common_atomic_graph(
666666
Parameters
667667
----------
668668
atype
669-
(N,) flat LOCAL atom types, ``N == sum(n_node)``.
669+
(N,) flat local-plus-halo atom types, ``N == sum(n_node)``.
670670
n_node
671-
(nf,) per-frame local atom counts.
671+
(nf,) per-frame total node counts.
672672
edge_index
673673
(2, E) ``[src, dst]`` edge endpoints (flat local indices).
674674
edge_vec
675675
(E, 3) neighbor-minus-center edge vectors.
676676
edge_mask
677677
(E,) boolean/0-1 valid-edge mask.
678678
n_local
679-
Per-rank local atom counts for multi-rank inference. Ignored in
680-
PR-A (single-rank); accepted for ABI stability.
679+
Per-frame owned node counts. Halo fitting outputs are masked.
681680
fparam
682681
Frame parameter, ``(nf, ndf)``.
683682
aparam
684683
Atomic parameter, ``(N, nda)``.
685684
comm_dict
686-
MPI communication metadata. Ignored in PR-A; accepted for ABI
687-
stability.
685+
Optional MPI communication metadata.
688686
charge_spin
689-
charge/spin conditioning. Ignored in PR-A; accepted for ABI
690-
stability with charge/spin-conditioned descriptors.
687+
Charge/spin conditioning.
691688
692689
Returns
693690
-------
@@ -701,6 +698,7 @@ def forward_common_atomic_graph(
701698
edge_index=edge_index,
702699
edge_vec=edge_vec,
703700
edge_mask=edge_mask,
701+
n_local=n_local,
704702
)
705703
atomic_ret = self.atomic_model.forward_common_atomic_graph(
706704
graph, atype, fparam=fparam, aparam=aparam, charge_spin=charge_spin
@@ -739,28 +737,25 @@ def call_common_lower_graph(
739737
Parameters
740738
----------
741739
atype
742-
(N,) flat LOCAL atom types, ``N == sum(n_node)``.
740+
(N,) flat local-plus-halo atom types, ``N == sum(n_node)``.
743741
n_node
744-
(nf,) per-frame local atom counts.
742+
(nf,) per-frame total node counts.
745743
edge_index
746744
(2, E) ``[src, dst]`` edge endpoints (flat local indices).
747745
edge_vec
748746
(E, 3) neighbor-minus-center edge vectors.
749747
edge_mask
750748
(E,) boolean/0-1 valid-edge mask.
751749
n_local
752-
Per-rank local atom counts for multi-rank inference. Ignored in
753-
PR-A (single-rank); accepted for ABI stability.
750+
Per-frame owned node counts. Halo fitting outputs are masked.
754751
fparam
755752
Frame parameter, ``(nf, ndf)``.
756753
aparam
757754
Atomic parameter, ``(N, nda)``.
758755
comm_dict
759-
MPI communication metadata. Ignored in PR-A; accepted for ABI
760-
stability.
756+
Optional MPI communication metadata.
761757
charge_spin
762-
charge/spin conditioning. Ignored in PR-A; accepted for ABI
763-
stability with charge/spin-conditioned descriptors.
758+
Charge/spin conditioning.
764759
765760
Returns
766761
-------

deepmd/dpmodel/utils/neighbor_graph/__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
44
The unified edge/graph neighbor-list contract and its supporting machinery:
55
``graph`` (the ``NeighborGraph``/``GraphLayout`` contract + derived node-validity
6-
+ edge padding), ``builder`` (the carry-all ``build_neighbor_graph`` dispatcher +
7-
the ``from_dense_quartet`` legacy converter), ``segment`` (mask-aware
6+
+ edge padding), ``csr`` (backend-agnostic CSR construction and canonicalization),
7+
``builder`` (the carry-all ``build_neighbor_graph`` dispatcher + the
8+
``from_dense_quartet`` legacy converter), ``segment`` (mask-aware
89
segment-reduction toolkit), and ``derivatives`` (edge force/virial assembly).
910
See the design discussion wanghan-iapcm/deepmd-kit#4.
1011
"""
@@ -25,6 +26,11 @@
2526
from_dense_quartet,
2627
graph_from_dense_quartet,
2728
)
29+
from .csr import (
30+
attach_edge_csr,
31+
build_edge_csr,
32+
canonicalize_neighbor_graph,
33+
)
2834
from .derivatives import (
2935
edge_force_virial,
3036
)
@@ -39,6 +45,7 @@
3945
NeighborGraph,
4046
apply_pair_exclusion,
4147
frame_id_from_n_node,
48+
node_ownership_mask,
4249
node_validity_mask,
4350
pad_and_guard_angles,
4451
pad_and_guard_edges,
@@ -61,9 +68,12 @@
6168
"angle_to_node_sum",
6269
"apply_pair_exclusion",
6370
"attach_angles",
71+
"attach_edge_csr",
6472
"build_angle_index",
73+
"build_edge_csr",
6574
"build_neighbor_graph",
6675
"build_neighbor_graph_ase",
76+
"canonicalize_neighbor_graph",
6777
"center_edge_pairs",
6878
"edge_env_mat",
6979
"edge_force_virial",
@@ -72,6 +82,7 @@
7282
"graph_angle_cos",
7383
"graph_from_dense_quartet",
7484
"neighbor_graph_from_ijs",
85+
"node_ownership_mask",
7586
"node_validity_mask",
7687
"pad_and_guard_angles",
7788
"pad_and_guard_edges",

deepmd/dpmodel/utils/neighbor_graph/angles.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# SPDX-License-Identifier: LGPL-3.0-or-later
22
"""3-body angle graph: pairs of edges sharing a center within a_rcut.
33
4-
Angles reference EDGES (angle_index into [0,E)); edge_vec stays the only
5-
geometry leaf. a_sel is normalization-only (not a truncation). Reuses PR-D's
6-
center_edge_pairs; a_rcut filters the participating edges.
4+
Angles reference edges (angle_index into [0,E)); edge_vec stays the only
5+
geometry leaf. a_sel is normalization-only (not a truncation), and a_rcut
6+
filters the edges passed to center_edge_pairs.
77
"""
88

99
from __future__ import (
@@ -89,8 +89,8 @@ def build_angle_index(
8989
# the normalization below.
9090
dist = safe_for_vector_norm(edge_vec, axis=-1) # (E,)
9191
a_edge_mask = xp.astype(edge_mask, xp.bool) & (dist < a_rcut)
92-
# compact eager form only (static_nnei not exposed until angle export is
93-
# needed, PR-G). dst = edge_index[1, :] per the [src, dst] SoA convention.
92+
# The compact eager form groups by destination under the [src, dst] SoA
93+
# convention.
9494
q_e, k_e, pair_mask = center_edge_pairs(
9595
edge_index[1, :],
9696
a_edge_mask,

0 commit comments

Comments
 (0)