Skip to content

Commit f54e820

Browse files
committed
feat(pt_expt): call-time DeepEval auto ladder with nf==1 vesin gate
Extract resolve_auto_graph_builder for inference and select vesin only for single-frame batches, matching _select_neighbor_builder. Multi-frame auto stays on nv/dense so auto_batch_size does not hit the per-frame loop. Signed-off-by: shaurya2k06 <shaurya2k06@gmail.com>
1 parent cc689a7 commit f54e820

162 files changed

Lines changed: 14865 additions & 4834 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.

.github/workflows/build_wheel.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ jobs:
5656
# https://github.com/pypa/setuptools_scm/issues/480
5757
fetch-depth: 0
5858
- name: Install uv
59-
run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.2.24/uv-installer.sh | sh
59+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
60+
with:
61+
version: "0.12.0"
6062
if: runner.os != 'Linux'
6163
- name: Build wheels
6264
uses: pypa/cibuildwheel@v4.1
@@ -68,6 +70,9 @@ jobs:
6870
CUDA_VERSION: ${{ matrix.cuda_version }}
6971
DP_PKG_NAME: ${{ matrix.dp_pkg_name }}
7072
CIBW_BUILD_FRONTEND: "build[uv]"
73+
# uv defaults to 50 concurrent downloads, which can exhaust the
74+
# macOS runner's socket buffers while installing wheel test extras.
75+
UV_CONCURRENT_DOWNLOADS: ${{ startsWith(matrix.platform_id, 'macosx_') && '8' || '50' }}
7176
- uses: actions/upload-artifact@v7
7277
with:
7378
name: cibw-cp${{ matrix.python }}-${{ matrix.platform_id }}-cu${{ matrix.cuda_version }}-${{ strategy.job-index }}

.github/workflows/test_cc.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ jobs:
1515
testcc:
1616
name: Test C++
1717
runs-on: ${{ matrix.check_memleak == false && github.repository_owner == 'deepmodeling' && 'cpu' || 'ubuntu-22.04' }}
18+
# Paddle's external interface download is occasionally unavailable. Keep
19+
# exercising these configurations without making that outage blocking.
20+
continue-on-error: ${{ matrix.enable_paddle }}
1821
strategy:
22+
# An allowed Paddle failure must not cancel the other backend jobs.
23+
fail-fast: false
1924
matrix:
2025
# Only regular jobs run the LAMMPS tests. Use self-hosted CPU runners
2126
# for those jobs in deepmodeling, with a hosted fallback for forks.

.github/workflows/test_cuda.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ concurrency:
1313
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
1414
cancel-in-progress: true
1515
name: Test CUDA
16+
env:
17+
# Temporarily disable Paddle in CUDA CI while its external downloads are
18+
# unreliable. Keep this switch explicit so the coverage can be restored.
19+
DP_CI_ALLOW_MISSING_PADDLE: "1"
20+
DP_ENABLE_PADDLE: "0"
1621
jobs:
1722
test_python:
1823
name: Test Python on CUDA
@@ -53,7 +58,6 @@ jobs:
5358
- run: |
5459
export PYTORCH_ROOT=$(python -c 'import torch;print(torch.__path__[0])')
5560
export TENSORFLOW_ROOT=$(python -c 'import importlib.util,pathlib;print(pathlib.Path(importlib.util.find_spec("tensorflow").origin).parent)')
56-
pip install --find-links "https://www.paddlepaddle.org.cn/packages/nightly/cu126/paddlepaddle-gpu/" --index-url https://pypi.org/simple --trusted-host www.paddlepaddle.org.cn --trusted-host paddlepaddle.org.cn "paddlepaddle-gpu==3.4.0.dev20260310"
5761
source/install/uv_with_retry.sh pip install --system -v -e .[gpu,test,lmp,cu12,torch,jax] mpi4py --reinstall-package deepmd-kit
5862
# See https://github.com/jax-ml/jax/issues/29042
5963
source/install/uv_with_retry.sh pip install --system -U 'nvidia-cublas-cu12>=12.9.0.13'
@@ -62,7 +66,7 @@ jobs:
6266
DP_ENABLE_NATIVE_OPTIMIZATION: 1
6367
DP_ENABLE_PYTORCH: 1
6468
- run: dp --version
65-
- run: python -m pytest source/tests
69+
- run: python -m pytest source/tests --ignore=source/tests/pd
6670
env:
6771
NUM_WORKERS: 0
6872
CUDA_VISIBLE_DEVICES: 0
@@ -106,7 +110,6 @@ jobs:
106110
- run: |
107111
export PYTORCH_ROOT=$(python -c 'import torch;print(torch.__path__[0])')
108112
export TENSORFLOW_ROOT=$(python -c 'import importlib.util,pathlib;print(pathlib.Path(importlib.util.find_spec("tensorflow").origin).parent)')
109-
pip install --find-links "https://www.paddlepaddle.org.cn/packages/nightly/cu126/paddlepaddle-gpu/" --index-url https://pypi.org/simple --trusted-host www.paddlepaddle.org.cn --trusted-host paddlepaddle.org.cn "paddlepaddle-gpu==3.4.0.dev20260310"
110113
source/install/uv_with_retry.sh pip install --system -v -e .[gpu,test,lmp,cu12,torch,jax] mpi4py --reinstall-package deepmd-kit
111114
# See https://github.com/jax-ml/jax/issues/29042
112115
source/install/uv_with_retry.sh pip install --system -U 'nvidia-cublas-cu12>=12.9.0.13'
@@ -127,12 +130,10 @@ jobs:
127130
CMAKE_GENERATOR: Ninja
128131
DP_VARIANT: cuda
129132
DP_USE_MPICH2: 1
133+
ENABLE_PADDLE: FALSE
130134
- run: |
131135
export LD_LIBRARY_PATH=$CUDA_PATH/lib64:/usr/lib/x86_64-linux-gnu/:$GITHUB_WORKSPACE/dp_test/lib:$LD_LIBRARY_PATH
132136
export PATH=$GITHUB_WORKSPACE/dp_test/bin:$PATH
133-
cp $GITHUB_WORKSPACE/source/build_tests/paddle_inference_install_dir/paddle/lib/* $GITHUB_WORKSPACE/dp_test/lib/
134-
cp $GITHUB_WORKSPACE/source/build_tests/paddle_inference_install_dir/third_party/install/onednn/lib/* $GITHUB_WORKSPACE/dp_test/lib/
135-
cp $GITHUB_WORKSPACE/source/build_tests/paddle_inference_install_dir/third_party/install/mklml/lib/* $GITHUB_WORKSPACE/dp_test/lib/
136137
python -m pytest -s source/lmp/tests || (cat log.lammps && exit 1)
137138
python -m pytest source/ipi/tests
138139
env:
@@ -141,6 +142,7 @@ jobs:
141142
TF_INTER_OP_PARALLELISM_THREADS: 1
142143
LAMMPS_PLUGIN_PATH: ${{ github.workspace }}/dp_test/lib/deepmd_lmp
143144
CUDA_VISIBLE_DEVICES: 0
145+
ENABLE_PADDLE: 0
144146
pass:
145147
name: Pass testing on CUDA
146148
needs: [test_python, test_cc]

deepmd/dpmodel/array_api.py

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,22 @@ def xp_add_at(x: Array, indices: Array, values: Array) -> Array:
201201
import torch
202202

203203
return torch.index_add(x, 0, indices, values)
204+
elif getattr(xp, "__name__", "") == "deepmd._vendors.ndtensorflow":
205+
import tensorflow as tf
206+
207+
x_tensor = x.unwrap()
208+
indices_tensor = tf.reshape(tf.cast(indices.unwrap(), tf.int64), (-1,))
209+
values_tensor = values.unwrap()
210+
# unsorted_segment_sum rather than scatter_nd: both accumulate repeated
211+
# indices, but scatter_nd rejects a destination with no elements even
212+
# when the updates are empty too, which a descriptor call with zero
213+
# edges legitimately produces.
214+
updates = tf.math.unsorted_segment_sum(
215+
values_tensor,
216+
indices_tensor,
217+
tf.shape(x_tensor, out_type=tf.int64)[0],
218+
)
219+
return xp.asarray(x_tensor + updates)
204220
else:
205221
# Fallback for array_api_strict: use basic indexing only
206222
# may need a more efficient way to do this
@@ -270,6 +286,52 @@ def xp_maximum_at(x: Array, indices: Array, values: Array) -> Array:
270286
return torch.scatter_reduce(
271287
x, 0, index, values, reduce="amax", include_self=True
272288
)
289+
elif getattr(xp, "__name__", "") == "deepmd._vendors.ndtensorflow":
290+
import tensorflow as tf
291+
292+
x_tensor = x.unwrap()
293+
indices_tensor = tf.reshape(tf.cast(indices.unwrap(), tf.int64), (-1,))
294+
values_tensor = values.unwrap()
295+
reduced = tf.math.unsorted_segment_max(
296+
values_tensor,
297+
indices_tensor,
298+
tf.shape(x_tensor, out_type=tf.int64)[0],
299+
)
300+
if values_tensor.dtype.is_floating:
301+
# TensorFlow uses the lowest finite value as the identity of
302+
# unsorted_segment_max. Restore the true maximum-at identity when
303+
# every update for a touched segment element is negative infinity.
304+
all_negative_infinity = (
305+
tf.math.unsorted_segment_min(
306+
tf.cast(
307+
tf.math.is_inf(values_tensor) & (values_tensor < 0),
308+
tf.int32,
309+
),
310+
indices_tensor,
311+
tf.shape(x_tensor, out_type=tf.int64)[0],
312+
)
313+
> 0
314+
)
315+
reduced = tf.where(
316+
all_negative_infinity,
317+
tf.cast(float("-inf"), values_tensor.dtype),
318+
reduced,
319+
)
320+
segment_counts = tf.math.unsorted_segment_sum(
321+
tf.ones_like(indices_tensor, dtype=tf.int32),
322+
indices_tensor,
323+
tf.shape(x_tensor, out_type=tf.int64)[0],
324+
)
325+
touched = segment_counts > 0
326+
touched_shape = tf.concat(
327+
[
328+
tf.reshape(tf.shape(x_tensor, out_type=tf.int64)[0], (1,)),
329+
tf.ones(tf.rank(x_tensor) - 1, dtype=tf.int64),
330+
],
331+
axis=0,
332+
)
333+
touched = tf.reshape(touched, touched_shape)
334+
return xp.asarray(tf.where(touched, tf.maximum(x_tensor, reduced), x_tensor))
273335
else:
274336
# Fallback for array_api_strict: basic indexing only.
275337
n = indices.shape[0]
@@ -337,12 +399,12 @@ def xp_setitem_at(x: Array, mask: Array, values: Array) -> Array:
337399
def xp_uniform(like: Array, size: int, low: float = 0.0, high: float = 1.0) -> Array:
338400
"""Draw ``size`` uniform samples in ``[low, high)`` on ``like``'s device.
339401
340-
Each backend uses its own generator: torch draws with ``torch.rand`` (as
341-
pt does, so ``setup_seed`` replays it, with no host copy -- and a host
342-
draw would freeze to a constant under tracing); other backends use
343-
:mod:`deepmd.utils.random`, which ``setup_seed`` also seeds. Draws are
344-
therefore not comparable across backends -- use only for a per-forward
345-
random stream, never where a parity test looks.
402+
Each backend uses its own generator: TensorFlow draws with
403+
``tf.random.uniform`` so traced graphs advance the runtime RNG, torch draws
404+
with ``torch.rand`` (so ``setup_seed`` replays it without a host copy), and
405+
other backends use :mod:`deepmd.utils.random`. Draws are therefore not
406+
comparable across backends -- use only for a per-forward random stream,
407+
never where a parity test looks.
346408
347409
Parameters
348410
----------
@@ -360,6 +422,18 @@ def xp_uniform(like: Array, size: int, low: float = 0.0, high: float = 1.0) -> A
360422
Array
361423
Samples of shape ``(size,)`` matching ``like``.
362424
"""
425+
xp = array_api_compat.array_namespace(like)
426+
if getattr(xp, "__name__", "") == "deepmd._vendors.ndtensorflow":
427+
import tensorflow as tf
428+
429+
sample_shape = tf.reshape(tf.cast(size, tf.int32), (1,))
430+
samples = tf.random.uniform(
431+
sample_shape,
432+
minval=low,
433+
maxval=high,
434+
dtype=like.dtype,
435+
)
436+
return xp.asarray(samples)
363437
if array_api_compat.is_torch_array(like):
364438
import torch
365439

@@ -368,7 +442,6 @@ def xp_uniform(like: Array, size: int, low: float = 0.0, high: float = 1.0) -> A
368442
)
369443
from deepmd.utils import random as dp_random
370444

371-
xp = array_api_compat.array_namespace(like)
372445
drawn = np.asarray(dp_random.random(size)) * (high - low) + low
373446
return xp.astype(
374447
xp_asarray_nodetach(xp, drawn, device=array_api_compat.device(like)),

deepmd/dpmodel/atomic_model/base_atomic_model.py

Lines changed: 120 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,54 @@ def supports_native_spin(self) -> bool:
190190
"""
191191
return False
192192

193+
def has_message_passing_across_ranks(self) -> bool:
194+
"""Whether multi-rank inference needs a cross-rank ghost exchange.
195+
196+
Generic capability (concrete default ``False``): the export layer
197+
consults it instead of reaching for a descriptor, so the answer
198+
stays correct for descriptor-less models and compositions.
199+
"""
200+
return False
201+
202+
def supports_edge_parallel(self) -> bool:
203+
"""Whether this atomic model can run under MPI domain decomposition.
204+
205+
Default ``True``; a model folding state no single rank observes
206+
(e.g. SFPG bridging before its exchange lands) overrides to False.
207+
"""
208+
return True
209+
210+
def dense_lower_supports_comm(self) -> bool:
211+
"""Whether the DENSE (nlist) lower implements comm_dict exchange.
212+
213+
Default ``True`` — dense comm is the production multi-rank path for
214+
dpa2/dpa3; DPA4's dense adapter raises on comm_dict and overrides
215+
via its descriptor.
216+
"""
217+
return True
218+
219+
def uses_compact_edge_pairs(self) -> bool:
220+
"""Whether the graph lower emits compact ``center_edge_pairs``
221+
(drives the torch>=2.6 unbacked-SymInt export guard).
222+
"""
223+
return False
224+
225+
def graph_edge_dtype(self) -> str:
226+
"""Edge-geometry dtype the graph deployment artifact accepts.
227+
228+
``"float64"`` is the model-agnostic ABI; geometrically compressed
229+
float32 descriptors override to ``"float32"``.
230+
"""
231+
return "float64"
232+
233+
def supports_graph_export(self) -> bool:
234+
"""Whether an exportable graph-lower implementation exists.
235+
236+
A compressed descriptor without its fused opaque operator cannot be
237+
traced through the reference tabulation kernel.
238+
"""
239+
return True
240+
193241
def get_default_fparam(self) -> list[float] | None:
194242
"""Get the default frame parameters."""
195243
return None
@@ -805,9 +853,22 @@ def _store_out_stat(
805853
self.out_std = out_std_data
806854

807855
def _get_forward_wrapper_func(self) -> Callable[..., dict[str, np.ndarray]]:
808-
"""Get a forward wrapper of the atomic model for output bias calculation."""
856+
"""Get a forward wrapper of the atomic model for output bias calculation.
857+
858+
The wrapper starts from raw coordinates and therefore has to construct
859+
the neighbor representation itself. It builds the one this model
860+
declares through :meth:`uses_graph_lower`: a carry-all
861+
``NeighborGraph`` for graph-native models, whose neighbor count follows
862+
the geometry, or the fixed-capacity neighbor list sized by
863+
:meth:`get_sel` otherwise. Sizing a dense list from ``get_sel`` is not
864+
merely wasteful for a graph-native model -- such a model reports no
865+
finite capacity, so the allocation is unbounded.
866+
"""
809867
import array_api_compat
810868

869+
from deepmd.dpmodel.utils.neighbor_graph import (
870+
build_neighbor_graph,
871+
)
811872
from deepmd.dpmodel.utils.nlist import (
812873
extend_input_and_build_neighbor_list,
813874
)
@@ -841,31 +902,64 @@ def model_forward(
841902
if charge_spin is not None:
842903
charge_spin = xp.asarray(charge_spin, device=device)
843904

844-
(
845-
extended_coord,
846-
extended_atype,
847-
mapping,
848-
nlist,
849-
) = extend_input_and_build_neighbor_list(
850-
coord,
851-
atype,
852-
self.get_rcut(),
853-
self.get_sel(),
854-
mixed_types=self.mixed_types(),
855-
box=box,
856-
# exclusion is a nlist-BUILD transform (decision #18/A4);
857-
# forward_common_atomic consumes a pre-excluded nlist.
858-
pair_excl=self.pair_excl,
859-
)
860-
atomic_ret = self.forward_common_atomic(
861-
extended_coord,
862-
extended_atype,
863-
nlist,
864-
mapping=mapping,
865-
fparam=fparam,
866-
aparam=aparam,
867-
charge_spin=charge_spin,
868-
)
905+
if self.uses_graph_lower():
906+
nframes, nloc = atype.shape
907+
# Pair exclusion is a neighbor-BUILD transform (decision
908+
# #18/A4) on both routes; the graph builder folds it into
909+
# ``edge_mask``.
910+
graph = build_neighbor_graph(
911+
coord,
912+
atype,
913+
box,
914+
self.get_rcut(),
915+
pair_excl=self.pair_excl,
916+
)
917+
atomic_ret = self.forward_common_atomic_graph(
918+
graph,
919+
xp.reshape(atype, (-1,)),
920+
fparam=fparam,
921+
aparam=(
922+
xp.reshape(
923+
aparam,
924+
(nframes * nloc, self.get_dim_aparam()),
925+
)
926+
if aparam is not None
927+
else None
928+
),
929+
charge_spin=charge_spin,
930+
)
931+
# The graph route works on a flat node axis; restore the
932+
# per-frame layout the dense route returns.
933+
atomic_ret = {
934+
kk: xp.reshape(vv, (nframes, nloc, *vv.shape[1:]))
935+
for kk, vv in atomic_ret.items()
936+
}
937+
else:
938+
(
939+
extended_coord,
940+
extended_atype,
941+
mapping,
942+
nlist,
943+
) = extend_input_and_build_neighbor_list(
944+
coord,
945+
atype,
946+
self.get_rcut(),
947+
self.get_sel(),
948+
mixed_types=self.mixed_types(),
949+
box=box,
950+
# exclusion is a nlist-BUILD transform (decision #18/A4);
951+
# forward_common_atomic consumes a pre-excluded nlist.
952+
pair_excl=self.pair_excl,
953+
)
954+
atomic_ret = self.forward_common_atomic(
955+
extended_coord,
956+
extended_atype,
957+
nlist,
958+
mapping=mapping,
959+
fparam=fparam,
960+
aparam=aparam,
961+
charge_spin=charge_spin,
962+
)
869963
# Convert outputs back to numpy arrays
870964
return {kk: to_numpy_array(vv) for kk, vv in atomic_ret.items()}
871965

0 commit comments

Comments
 (0)