Skip to content

Commit 6330a2f

Browse files
njzjznjzjz-botpre-commit-ci[bot]OutisLi
authored
feat(tf2): support DPA4 descriptor (deepmodeling#5749)
## Summary - Add and register the TF2 DPA4/SeZM descriptor adapter and its TensorFlow-backed submodule mappings. - Add the TensorFlow array operations, dynamic-shape handling, and trackable parameter support required by the DPA4 descriptor. - Enable TF2 coverage in the existing DPA4 descriptor consistency tests and add focused descriptor state, checkpoint, graph-shape, and array-operation tests. - Keep this pull request descriptor-only; fitting, model factory, trainer, and model-conversion changes are excluded. ## Validation - ruff format . - ruff check . - python -m py_compile deepmd/dpmodel/array_api.py deepmd/dpmodel/descriptor/dpa4.py deepmd/dpmodel/descriptor/dpa4_nn/so2.py deepmd/tf2/common.py deepmd/tf2/descriptor/dpa4.py source/tests/consistent/descriptor/test_dpa4.py source/tests/consistent/test_array_api.py source/tests/tf2/test_dpa4.py - DP_TEST_TF2_ONLY=1 pytest source/tests/tf2/test_dpa4.py -v — 6 passed - DP_TEST_TF2_ONLY=1 pytest source/tests/consistent/test_array_api.py::TestXpMaximumAtConsistent::test_tf_preserves_all_negative_infinity_segment -v — 1 passed Coding agent: Codex Codex version: codex-cli 0.144.4 Model: gpt-5.6-sol Reasoning effort: xhigh --------- Co-authored-by: njzjz-bot <njzjz.bot@gmail.com> Co-authored-by: njzjz-bot <njzjz-bot@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: OutisLi <137472077+OutisLi@users.noreply.github.com>
1 parent 9bc4eb5 commit 6330a2f

11 files changed

Lines changed: 996 additions & 19 deletions

File tree

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/descriptor/dpa4_nn/so2.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ def _project_radial(self, radial_feat: Array) -> Array:
665665
device = array_api_compat.device(radial_feat)
666666
radial_m0 = xp.reshape(
667667
radial_feat[:, : self.lmax + 1, :],
668-
(radial_feat.shape[0], self.input_dim),
668+
(-1, self.input_dim),
669669
)
670670
weight = xp_asarray_nodetach(xp, self.weight[...], device=device)
671671
return xp.matmul(radial_m0, weight)
@@ -744,9 +744,45 @@ def call(self, x_local: Array, radial_feat: Array) -> Array:
744744
Invariant radial/type features with shape (E, D_m, C_wide).
745745
"""
746746
xp = array_api_compat.array_namespace(x_local)
747-
if x_local.shape != radial_feat.shape:
747+
x_shape = x_local.shape
748+
radial_shape = radial_feat.shape
749+
750+
def static_rank(shape: Any) -> int | None:
751+
rank = getattr(shape, "rank", None)
752+
if rank is not None:
753+
return int(rank)
754+
try:
755+
return len(shape)
756+
except (TypeError, ValueError):
757+
return None
758+
759+
def static_dim(shape: Any, axis: int) -> int | None:
760+
try:
761+
dim = shape[axis]
762+
except (IndexError, TypeError, ValueError):
763+
return None
764+
dim = getattr(dim, "value", dim)
765+
return int(dim) if isinstance(dim, (int, np.integer)) else None
766+
767+
x_rank = static_rank(x_shape)
768+
radial_rank = static_rank(radial_shape)
769+
if (x_rank is not None and x_rank != 3) or (
770+
radial_rank is not None and radial_rank != 3
771+
):
772+
raise ValueError("DynamicRadialDegreeMixer inputs must have rank 3")
773+
if any(
774+
x_dim is not None and radial_dim is not None and x_dim != radial_dim
775+
for x_dim, radial_dim in (
776+
(static_dim(x_shape, axis), static_dim(radial_shape, axis))
777+
for axis in range(3)
778+
)
779+
):
748780
raise ValueError("`x_local` and `radial_feat` must have the same shape")
749-
if x_local.shape[1] != self.reduced_dim or x_local.shape[2] != self.channels:
781+
reduced_dim = static_dim(x_shape, 1)
782+
channel_dim = static_dim(x_shape, 2)
783+
if (reduced_dim is not None and reduced_dim != self.reduced_dim) or (
784+
channel_dim is not None and channel_dim != self.channels
785+
):
750786
raise ValueError("Input shape is incompatible with this mixer")
751787

752788
kernel_flat = self._project_radial(radial_feat)
@@ -755,14 +791,10 @@ def call(self, x_local: Array, radial_feat: Array) -> Array:
755791
return xp.matmul(kernel, x_local)
756792

757793
if self.rank > 0:
758-
compact = xp.reshape(
759-
kernel_flat, (x_local.shape[0], self.degree_kernel_size, self.rank)
760-
)
794+
compact = xp.reshape(kernel_flat, (-1, self.degree_kernel_size, self.rank))
761795
return self._mix_rank_compact(compact, x_local)
762796

763-
compact = xp.reshape(
764-
kernel_flat, (x_local.shape[0], self.degree_kernel_size, self.channels)
765-
)
797+
compact = xp.reshape(kernel_flat, (-1, self.degree_kernel_size, self.channels))
766798
kernel = self._scatter_channel_kernel(compact)
767799
# einsum("eoic,eic->eoc"): contract l_in i per channel c (no channel mix).
768800
return xp.sum(kernel * x_local[:, None, :, :], axis=2)
@@ -791,12 +823,12 @@ def _mix_rank_compact(self, compact: Array, x_local: Array) -> Array:
791823
# via a single matmul, then weight the rank channels by channel_basis.
792824
kernel_or = xp.reshape(
793825
xp.permute_dims(kernel, (0, 1, 3, 2)),
794-
(x_local.shape[0], self.reduced_dim * self.rank, self.reduced_dim),
826+
(-1, self.reduced_dim * self.rank, self.reduced_dim),
795827
)
796828
mixed = xp.matmul(kernel_or, x_local)
797829
mixed = xp.reshape(
798830
mixed,
799-
(x_local.shape[0], self.reduced_dim, self.rank, self.channels),
831+
(-1, self.reduced_dim, self.rank, self.channels),
800832
)
801833
channel_basis = xp.reshape(
802834
xp_asarray_nodetach(xp, self.channel_basis[...], device=device),

0 commit comments

Comments
 (0)