Skip to content

Commit 480509a

Browse files
OutisLipre-commit-ci[bot]claude
authored
perf(pt): DPA4-family performance optimizations (deepmodeling#6001)
## Summary - accelerate SeZM/DPA4 inference with tuned Triton kernels, fused cuTile and CUDA paths, and lower projection overhead - bring the accelerated inference and force-loss training paths to the `pt_expt` backend while keeping common tensor math in `dpmodel` - add fused DPA4C CPU/CUDA graph execution, including graph construction, fitting, force, and virial paths - reduce distributed-training overhead and capture the HybridMuon update in a CUDA graph - keep unsupported layouts, distributed precompile, and CPU-traced exports on explicit reference or target-aware fallback seams ## Performance On the documented RTX PRO 6000 Blackwell workloads: - an 8,000-atom DPA4-mini force step improves from 5.72 ms to 4.28 ms; the compiled package improves from 6.03 ms to 4.40 ms - the fused CUDA lower graph improves from 117.5 ms to 74.0 ms and reduces peak memory from 15.4 GiB to 11.2 GiB - the same inference tuning improves the 48,640-atom force step by 1.30x and raises the measured 48 GiB capacity ceiling from about 40,000 to 48,640 atoms The kernel levels remain opt-in and target-aware. Unsupported shapes and devices retain the reference implementations. ## Validation - `git diff --check upstream/master..HEAD` - `ruff check` on all 154 changed Python files - CPU array-API and DPA4C paths: 72 passed, 3 skipped - CUDA serialization kernel-level policy: 11 passed - PT and PT-expt accelerated training paths: 12 passed - HybridMuon and cuTile paths: 42 passed, 12 subtests passed - CPU-trace-to-CUDA fast-op export and AOT package paths: 4 passed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added accelerated DPA4/SeZM computation across CUDA, Triton, cuTile, and CPU. * Added CPU cell-based neighbor-graph construction and improved CSR handling. * Added optimized scalar-only readout and projection paths. * Added CUDA graph capture, training graph precompilation, and configurable compilation. * Added cross-backend einsum support and improved accelerator selection. * Preserved trainable radial-basis settings during serialization. * **Bug Fixes** * Corrected source-atom virial attribution and improved empty-graph and masked-edge handling. * Preserved autograd when moving arrays between devices. * **Documentation** * Expanded guidance for accelerated DPA4/DPA4C inference, export settings, precision, and hardware compatibility. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b32f74c commit 480509a

259 files changed

Lines changed: 49355 additions & 6520 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/array_api.py

Lines changed: 148 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-License-Identifier: LGPL-3.0-or-later
22
"""Utilities for the array API."""
33

4+
import math
45
from typing import (
56
Any,
67
)
@@ -40,14 +41,22 @@ def xp_asarray_nodetach(
4041
required device-to-host copy when a CUDA-backed model constant is consumed
4142
by a NumPy statistics path.
4243
43-
The ``device`` argument only applies to the conversion path. Arrays already
44-
in ``xp`` are assumed to live on the working device because model buffers
45-
and inputs are moved together.
44+
An array already in ``xp`` normally needs no move, since model buffers and
45+
inputs travel together. Tracing breaks that: a CPU export of a
46+
CUDA-resident model runs CPU inputs through a module whose buffers stayed
47+
on the device, and the mismatch surfaces far downstream as a fake-tensor
48+
device error. A requested ``device`` that differs is therefore honoured
49+
here, which costs a comparison on the hot path and a copy only in the
50+
tracing case.
4651
"""
4752
if array_api_compat.is_array_api_obj(obj):
4853
if array_api_compat.array_namespace(obj) is xp:
4954
if dtype is not None and obj.dtype != dtype:
5055
obj = xp.astype(obj, dtype)
56+
if device is not None and array_api_compat.device(obj) != device:
57+
# ``xp.asarray`` would detach, which is what this helper exists
58+
# to avoid; ``to_device`` is the array-API move that does not.
59+
obj = array_api_compat.to_device(obj, device)
5160
return obj
5261
obj = to_numpy_array(obj)
5362
if dtype is None:
@@ -111,6 +120,142 @@ def xp_take_along_axis(arr: Array, indices: Array, axis: int) -> Array:
111120
return xp_swapaxes(out, axis, -1)
112121

113122

123+
def xp_einsum(subscripts: str, *operands: Array) -> Array:
124+
"""Contract *operands* according to the Einstein summation *subscripts*.
125+
126+
The array API standard has no ``einsum``, so an array-API-only module has
127+
to express a contraction as a chain of ``permute_dims`` / ``reshape`` /
128+
``matmul``. That chain fixes one particular execution order, which is
129+
rarely the best one and is opaque to a compiler: a batched contraction
130+
written this way reaches PyTorch as ``bmm`` plus transposing copies, where
131+
the same expression given to ``torch.einsum`` is free to become a single
132+
``mm`` on a reshaped operand or to fuse into a neighbouring kernel. On the
133+
production DPA4 shapes the difference is measurable -- the array-API chain
134+
put roughly 150 more contractions per training step on ``bmm`` instead of
135+
``mm``, for about 5% of the step and an extra gigabyte of peak memory.
136+
137+
Writing the chain by hand is also a correctness-adjacent hazard, because
138+
the orders differ by more than a constant. The broadcast spelling
139+
``matmul(x[..., None, :], weight[None, ...])`` makes the node count the
140+
matmul batch, so the weight is expanded across it and autograd reduces
141+
that whole expansion back to the parameter shape: at production sizes a
142+
165 K-element weight became 191 M elements, and its reduce was the single
143+
costliest kernel of a training step (a 15x penalty on the affected
144+
contraction). Stating the contraction leaves that choice to the backend.
145+
146+
Every backend this project targets (NumPy, PyTorch, JAX) ships an
147+
``einsum``, so it is dispatched directly where available. The array-API
148+
fallback below serves the remaining namespaces (``array_api_strict``, used
149+
by the conformance tests) and is restricted to what those need.
150+
151+
Parameters
152+
----------
153+
subscripts : str
154+
Subscript specification in explicit form, e.g. ``"bfi,ifo->bfo"``.
155+
The implicit form (no ``->``) is rejected: it is ambiguous to the
156+
fallback and unused here.
157+
*operands : Array
158+
Arrays to contract, all from one namespace.
159+
160+
Returns
161+
-------
162+
Array
163+
The contraction result.
164+
165+
Raises
166+
------
167+
ValueError
168+
If *subscripts* is in implicit form, or if the fallback is reached
169+
with a specification it does not implement.
170+
"""
171+
if "->" not in subscripts:
172+
raise ValueError(f"xp_einsum requires an explicit output: {subscripts!r}")
173+
if array_api_compat.is_torch_array(operands[0]):
174+
import torch
175+
176+
return torch.einsum(subscripts, *operands)
177+
if array_api_compat.is_numpy_array(operands[0]):
178+
return np.einsum(subscripts, *operands)
179+
if array_api_compat.is_jax_array(operands[0]):
180+
import jax.numpy as jnp
181+
182+
return jnp.einsum(subscripts, *operands)
183+
return _xp_einsum_fallback(subscripts, *operands)
184+
185+
186+
def _xp_einsum_fallback(subscripts: str, *operands: Array) -> Array:
187+
"""Array-API-only ``einsum`` for a two-operand contraction.
188+
189+
Serves the namespaces without a native ``einsum``. The contraction is
190+
reduced to the canonical batched matmul: labels shared by both operands
191+
and the output are the batch, labels shared by the operands but absent
192+
from the output are contracted, and the rest are free on one side each.
193+
Each operand is permuted into ``(batch, free, contracted)`` order,
194+
flattened to three axes, multiplied, and restored to the requested output
195+
order.
196+
197+
Correctness rather than throughput is the aim here: the flattening
198+
materializes a copy of each operand whenever the permutation is not a
199+
view, which a native ``einsum`` would avoid. That trade is deliberate --
200+
every backend used for production has an ``einsum``, and this path exists
201+
for the conformance namespaces.
202+
203+
Only a diagonal (a label repeated within one operand) and an implicit
204+
output are rejected; both are absent from this codebase.
205+
"""
206+
xp = array_api_compat.array_namespace(*operands)
207+
inputs, output = subscripts.split("->")
208+
terms = inputs.split(",")
209+
if len(terms) != 2:
210+
raise ValueError(f"the array-API einsum fallback is binary: {subscripts!r}")
211+
left, right = terms
212+
lhs, rhs = operands
213+
for term, operand in ((left, lhs), (right, rhs)):
214+
if len(set(term)) != len(term):
215+
raise ValueError(f"a repeated label needs a diagonal: {subscripts!r}")
216+
if len(term) != operand.ndim:
217+
raise ValueError(f"{subscripts!r} does not match the operand ranks")
218+
if set(output) - (set(left) | set(right)):
219+
raise ValueError(f"the output carries an unknown label: {subscripts!r}")
220+
221+
# Classify every label by where it appears. Order follows the output for
222+
# the batch and free groups, so the final permutation is a short one.
223+
batch = [label for label in output if label in left and label in right]
224+
contracted = [label for label in left if label in right and label not in output]
225+
left_free = [label for label in output if label in left and label not in right]
226+
right_free = [label for label in output if label in right and label not in left]
227+
if set(left) - set(batch) - set(contracted) - set(left_free):
228+
raise ValueError(f"a label of the left operand vanishes: {subscripts!r}")
229+
if set(right) - set(batch) - set(contracted) - set(right_free):
230+
raise ValueError(f"a label of the right operand vanishes: {subscripts!r}")
231+
232+
def prepare(term: str, operand: Array, free: list[str], last: list[str]) -> Array:
233+
"""Permute to ``(batch, free, last)`` and flatten to three axes."""
234+
order = batch + free + last
235+
operand = xp.permute_dims(operand, tuple(term.index(l) for l in order))
236+
shape = operand.shape
237+
split = (len(batch), len(batch) + len(free))
238+
return xp.reshape(
239+
operand,
240+
(
241+
math.prod(shape[: split[0]]),
242+
math.prod(shape[split[0] : split[1]]),
243+
math.prod(shape[split[1] :]),
244+
),
245+
)
246+
247+
sizes = dict(zip(left, lhs.shape, strict=True)) | dict(
248+
zip(right, rhs.shape, strict=True)
249+
)
250+
out = xp.matmul(
251+
prepare(left, lhs, left_free, contracted),
252+
prepare(right, rhs, contracted, right_free),
253+
)
254+
order = batch + left_free + right_free
255+
out = xp.reshape(out, tuple(sizes[label] for label in order))
256+
return xp.permute_dims(out, tuple(order.index(label) for label in output))
257+
258+
114259
def xp_take_first_n(arr: Array, dim: int, n: int) -> Array:
115260
"""Take the first *n* elements along *dim*.
116261

0 commit comments

Comments
 (0)