Skip to content

Commit bcf3c54

Browse files
committed
perf(pt/dpa1): optimize graph CUDA inference and deployment
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 7c362d7 commit bcf3c54

100 files changed

Lines changed: 19890 additions & 888 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: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,42 @@ def forward_common_atomic(
313313
atom_mask = xp_take_first_n(ext_atom_mask, 1, nloc)
314314
return self._finalize_atomic_ret(ret_dict, atom_mask, atype)
315315

316+
def _prepare_graph_inputs(
317+
self,
318+
graph: "NeighborGraph",
319+
atype: Array,
320+
) -> tuple["NeighborGraph", Array, Array]:
321+
"""Apply graph masks shared by standard and fused atomic forwards."""
322+
xp = array_api_compat.array_namespace(graph.edge_vec)
323+
atype = xp.asarray(atype, device=array_api_compat.device(graph.edge_vec))
324+
atom_mask = self.make_atom_mask(atype) # (N,) bool
325+
atype_clamped = xp.where(atom_mask, atype, xp.zeros_like(atype))
326+
output_mask = atom_mask
327+
if graph.n_local is not None:
328+
from deepmd.dpmodel.utils.neighbor_graph import (
329+
node_ownership_mask,
330+
)
331+
332+
output_mask = output_mask & node_ownership_mask(
333+
graph.n_node,
334+
graph.n_local,
335+
atype.shape[0],
336+
)
337+
if self.pair_excl is not None:
338+
keep = self.pair_excl.build_edge_exclude_mask(
339+
graph.edge_index, atype_clamped
340+
)
341+
graph = dataclasses.replace(
342+
graph,
343+
edge_mask=graph.edge_mask * xp.astype(keep, graph.edge_mask.dtype),
344+
)
345+
if self.atom_excl is not None:
346+
output_mask = xp.logical_and(
347+
output_mask,
348+
self.atom_excl.build_type_exclude_mask(atype_clamped),
349+
)
350+
return graph, atype_clamped, output_mask
351+
316352
def forward_common_atomic_graph(
317353
self,
318354
graph: "NeighborGraph",
@@ -352,26 +388,15 @@ def forward_common_atomic_graph(
352388
the result dict on the flat node axis, defined by the `FittingOutputDef`.
353389
354390
"""
355-
xp = array_api_compat.array_namespace(graph.edge_vec)
356-
atype = xp.asarray(atype, device=array_api_compat.device(graph.edge_vec))
357-
atom_mask = self.make_atom_mask(atype) # (N,) bool
358-
atype_clamped = xp.where(atom_mask, atype, xp.zeros_like(atype))
359-
if self.pair_excl is not None:
360-
keep = self.pair_excl.build_edge_exclude_mask(
361-
graph.edge_index, atype_clamped
362-
)
363-
graph = dataclasses.replace(
364-
graph,
365-
edge_mask=graph.edge_mask * xp.astype(keep, graph.edge_mask.dtype),
366-
)
391+
graph, atype_clamped, output_mask = self._prepare_graph_inputs(graph, atype)
367392
ret_dict = self.forward_atomic_graph(
368393
graph,
369394
atype_clamped,
370395
fparam=fparam,
371396
aparam=aparam,
372397
charge_spin=charge_spin,
373398
)
374-
return self._finalize_atomic_ret(ret_dict, atom_mask, atype)
399+
return self._finalize_atomic_ret(ret_dict, output_mask, atype)
375400

376401
def _finalize_atomic_ret(
377402
self, ret_dict: dict, atom_mask: Array, atype: Array
@@ -399,10 +424,16 @@ def _finalize_atomic_ret(
399424
400425
"""
401426
xp = array_api_compat.array_namespace(atype)
402-
ret_dict = self.apply_out_stat(ret_dict, atype)
427+
safe_atype = xp.where(
428+
self.make_atom_mask(atype),
429+
atype,
430+
xp.zeros_like(atype),
431+
)
432+
ret_dict = self.apply_out_stat(ret_dict, safe_atype)
403433
if self.atom_excl is not None:
404434
atom_mask = xp.logical_and(
405-
atom_mask, self.atom_excl.build_type_exclude_mask(atype)
435+
atom_mask,
436+
self.atom_excl.build_type_exclude_mask(safe_atype),
406437
)
407438
lead = atom_mask.shape # (nf, nloc) dense | (N,) graph
408439
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
@@ -431,21 +431,25 @@ def uses_graph_lower(self) -> bool:
431431
"""Returns whether this descriptor supports the graph-native lower.
432432
433433
The graph-native lower (``call_graph``) covers the factorizable path
434-
AND transformer attention (``attn_layer >= 0``, NeighborGraph PR-D)
435-
with concat OR strip type-embedding. Remaining ineligible configs
436-
(``exclude_types``, and compressed descriptors) fall back to the legacy
437-
dense path, so those models keep working unchanged.
434+
and transformer attention (``attn_layer >= 0``) with concat or strip
435+
type-embedding. Geo-compressed strip models
436+
(``geo_compress=True``, ``attn_layer == 0``) are also graph-eligible;
437+
tebd-only compression (``geo_compress=False``) and ``exclude_types``
438+
stay on the legacy dense path.
438439
439440
Eligibility does NOT imply numerical interchangeability with the
440441
dense route for every config: with ``smooth_type_embedding=True``
441442
the carry-all graph attention is sel-independent by design and
442443
differs from the dense lower by up to ~1e-4 (see the Notes of
443444
:meth:`call_graph`).
444445
"""
445-
# compressed descriptors have no graph kernel (geo/tebd tabulation is
446-
# dense-only); keep them on the legacy dense path.
447446
if self.compress:
448-
return False
447+
return (
448+
self.geo_compress
449+
and self.se_atten.tebd_input_mode == "strip"
450+
and self.se_atten.attn_layer == 0
451+
and not self.se_atten.exclude_types
452+
)
449453
# exclude_types stays dense (graph exclusion is owned elsewhere); strip is
450454
# now graph-eligible (per-edge factorized embedding, no neighbor coupling).
451455
return (
@@ -1756,7 +1760,7 @@ def call_graph(
17561760
-----
17571761
Known limitations:
17581762
- ``tebd_input_mode`` in {"concat", "strip"}; compressed descriptors stay dense;
1759-
- ``exclude_types`` is not yet supported and raises (lands in a later PR).
1763+
- ``exclude_types`` is not supported and raises.
17601764
"""
17611765
from deepmd.dpmodel.utils.neighbor_graph import (
17621766
edge_env_mat,
@@ -1768,10 +1772,7 @@ def call_graph(
17681772
f"graph path does not support tebd_input_mode={self.tebd_input_mode!r}"
17691773
)
17701774
if self.exclude_types:
1771-
raise NotImplementedError(
1772-
"graph path does not yet apply exclude_types (NeighborGraph PR-A); "
1773-
"type exclusion lands in a later PR"
1774-
)
1775+
raise NotImplementedError("graph path does not support exclude_types")
17751776
if type_embedding is None:
17761777
raise ValueError("type_embedding is required for the graph path")
17771778
xp = array_api_compat.array_namespace(graph.edge_vec)

deepmd/dpmodel/model/make_model.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -641,28 +641,25 @@ def forward_common_atomic_graph(
641641
Parameters
642642
----------
643643
atype
644-
(N,) flat LOCAL atom types, ``N == sum(n_node)``.
644+
(N,) flat local-plus-halo atom types, ``N == sum(n_node)``.
645645
n_node
646-
(nf,) per-frame local atom counts.
646+
(nf,) per-frame total node counts.
647647
edge_index
648648
(2, E) ``[src, dst]`` edge endpoints (flat local indices).
649649
edge_vec
650650
(E, 3) neighbor-minus-center edge vectors.
651651
edge_mask
652652
(E,) boolean/0-1 valid-edge mask.
653653
n_local
654-
Per-rank local atom counts for multi-rank inference. Ignored in
655-
PR-A (single-rank); accepted for ABI stability.
654+
Per-frame owned node counts. Halo fitting outputs are masked.
656655
fparam
657656
Frame parameter, ``(nf, ndf)``.
658657
aparam
659658
Atomic parameter, ``(N, nda)``.
660659
comm_dict
661-
MPI communication metadata. Ignored in PR-A; accepted for ABI
662-
stability.
660+
Optional MPI communication metadata.
663661
charge_spin
664-
charge/spin conditioning. Ignored in PR-A; accepted for ABI
665-
stability with charge/spin-conditioned descriptors.
662+
Charge/spin conditioning.
666663
667664
Returns
668665
-------
@@ -676,6 +673,7 @@ def forward_common_atomic_graph(
676673
edge_index=edge_index,
677674
edge_vec=edge_vec,
678675
edge_mask=edge_mask,
676+
n_local=n_local,
679677
)
680678
atomic_ret = self.atomic_model.forward_common_atomic_graph(
681679
graph, atype, fparam=fparam, aparam=aparam, charge_spin=charge_spin
@@ -714,28 +712,25 @@ def call_common_lower_graph(
714712
Parameters
715713
----------
716714
atype
717-
(N,) flat LOCAL atom types, ``N == sum(n_node)``.
715+
(N,) flat local-plus-halo atom types, ``N == sum(n_node)``.
718716
n_node
719-
(nf,) per-frame local atom counts.
717+
(nf,) per-frame total node counts.
720718
edge_index
721719
(2, E) ``[src, dst]`` edge endpoints (flat local indices).
722720
edge_vec
723721
(E, 3) neighbor-minus-center edge vectors.
724722
edge_mask
725723
(E,) boolean/0-1 valid-edge mask.
726724
n_local
727-
Per-rank local atom counts for multi-rank inference. Ignored in
728-
PR-A (single-rank); accepted for ABI stability.
725+
Per-frame owned node counts. Halo fitting outputs are masked.
729726
fparam
730727
Frame parameter, ``(nf, ndf)``.
731728
aparam
732729
Atomic parameter, ``(N, nda)``.
733730
comm_dict
734-
MPI communication metadata. Ignored in PR-A; accepted for ABI
735-
stability.
731+
Optional MPI communication metadata.
736732
charge_spin
737-
charge/spin conditioning. Ignored in PR-A; accepted for ABI
738-
stability with charge/spin-conditioned descriptors.
733+
Charge/spin conditioning.
739734
740735
Returns
741736
-------

deepmd/dpmodel/utils/neighbor_graph/__init__.py

Lines changed: 11 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
"""
@@ -24,6 +25,10 @@
2425
build_neighbor_graph,
2526
from_dense_quartet,
2627
)
28+
from .csr import (
29+
build_edge_csr,
30+
canonicalize_neighbor_graph,
31+
)
2732
from .derivatives import (
2833
edge_force_virial,
2934
)
@@ -37,6 +42,7 @@
3742
GraphLayout,
3843
NeighborGraph,
3944
frame_id_from_n_node,
45+
node_ownership_mask,
4046
node_validity_mask,
4147
pad_and_guard_angles,
4248
pad_and_guard_edges,
@@ -59,15 +65,18 @@
5965
"angle_to_node_sum",
6066
"attach_angles",
6167
"build_angle_index",
68+
"build_edge_csr",
6269
"build_neighbor_graph",
6370
"build_neighbor_graph_ase",
71+
"canonicalize_neighbor_graph",
6472
"center_edge_pairs",
6573
"edge_env_mat",
6674
"edge_force_virial",
6775
"frame_id_from_n_node",
6876
"from_dense_quartet",
6977
"graph_angle_cos",
7078
"neighbor_graph_from_ijs",
79+
"node_ownership_mask",
7180
"node_validity_mask",
7281
"pad_and_guard_angles",
7382
"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)