Skip to content

Commit 90f032a

Browse files
author
njzjz-bot
committed
fix(tf): preserve ghost gradients in ProdForceGrad
Align the legacy ProdForce gradient with the forward op's nall-sized force output. Preserve each ghost atom's upstream gradient and validate neighbor indices, with a two-frame regression for the first ghost boundary. Coding-Agent: Codex Codex-Version: codex-cli 0.144.4 Model: gpt-5.6-sol Reasoning-Effort: xhigh
1 parent 6c3b985 commit 90f032a

2 files changed

Lines changed: 79 additions & 6 deletions

File tree

source/op/tf/prod_force_grad.cc

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class ProdForceGradOp : public OpKernel {
6868

6969
int nframes = net_deriv_tensor.shape().dim_size(0);
7070
int nloc = natoms(0);
71+
int nall = natoms(1);
7172
int ndescrpt = nloc > 0 ? net_deriv_tensor.shape().dim_size(1) / nloc : 0;
7273
int nnei = nloc > 0 ? nlist_tensor.shape().dim_size(1) / nloc : 0;
7374

@@ -85,9 +86,14 @@ class ProdForceGradOp : public OpKernel {
8586
context, (nframes == axis_shape.dim_size(0)),
8687
deepmd::tf_compat::InvalidArgument("number of frames should match"));
8788

88-
OP_REQUIRES(context, (nloc * 3 == grad_shape.dim_size(1)),
89+
OP_REQUIRES(
90+
context, (nall >= nloc),
91+
deepmd::tf_compat::InvalidArgument(
92+
"number of all atoms should not be smaller than local atoms"));
93+
OP_REQUIRES(context,
94+
(static_cast<int64_t>(nall) * 3 == grad_shape.dim_size(1)),
8995
deepmd::tf_compat::InvalidArgument(
90-
"input grad shape should be 3 x natoms"));
96+
"input grad shape should be 3 x all atoms"));
9197
OP_REQUIRES(context,
9298
(static_cast<int64_t>(nloc) * ndescrpt * 12 ==
9399
in_deriv_shape.dim_size(1)),
@@ -118,10 +124,20 @@ class ProdForceGradOp : public OpKernel {
118124
auto axis = axis_tensor.flat<int>();
119125
auto grad_net = grad_net_tensor->flat<FPTYPE>();
120126

127+
// ProdForce returns one force vector for every local and ghost atom. Keep
128+
// those upstream gradients distinct: folding ghost indices modulo nloc
129+
// would differentiate a different output than the forward op produced.
130+
const int64_t nlist_size = static_cast<int64_t>(nframes) * nloc * nnei;
131+
for (int64_t ii = 0; ii < nlist_size; ++ii) {
132+
OP_REQUIRES(context, nlist(ii) < nall,
133+
deepmd::tf_compat::InvalidArgument(
134+
"neighbor index should be smaller than all atoms"));
135+
}
136+
121137
// loop over frames
122138
#pragma omp parallel for
123139
for (int kk = 0; kk < nframes; ++kk) {
124-
int grad_iter = kk * nloc * 3;
140+
int grad_iter = kk * nall * 3;
125141
int net_iter = kk * nloc * ndescrpt;
126142
int in_iter = kk * nloc * ndescrpt * 12;
127143
int nlist_iter = kk * nloc * nnei;
@@ -163,9 +179,6 @@ class ProdForceGradOp : public OpKernel {
163179
// loop over neighbors
164180
for (int jj = 0; jj < nnei; ++jj) {
165181
int j_idx = nlist(nlist_iter + i_idx * nnei + jj);
166-
if (j_idx > nloc) {
167-
j_idx = j_idx % nloc;
168-
}
169182
if (j_idx < 0) {
170183
continue;
171184
}

source/tests/tf/test_prod_force_grad.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,70 @@
44
from deepmd.tf.env import (
55
GLOBAL_TF_FLOAT_PRECISION,
66
op_grads_module,
7+
op_module,
78
tf,
89
)
910

1011

12+
class TestLegacyProdForceGradGhost(tf.test.TestCase):
13+
"""Exercise the first ghost index through ProdForce's registered gradient."""
14+
15+
def test_first_ghost_uses_its_own_upstream_gradient(self) -> None:
16+
nloc = 1
17+
nall = 2
18+
nframes = 2
19+
ndescrpt = 4
20+
net_deriv = tf.placeholder(tf.float64, [nframes, nloc * ndescrpt])
21+
22+
in_deriv = np.zeros((nframes, nloc * ndescrpt * 12))
23+
# The only neighbor is axis 0. Give descriptor 0 a simple derivative
24+
# with respect to the ghost atom and leave every other term zero.
25+
in_deriv[:, 3:6] = [1.0, 2.0, 3.0]
26+
force = op_module.prod_force(
27+
net_deriv,
28+
tf.constant(in_deriv, dtype=tf.float64),
29+
tf.constant([[nloc], [nloc]], dtype=tf.int32),
30+
tf.constant([[0, 0, 0, 0], [0, 0, 0, 0]], dtype=tf.int32),
31+
tf.constant([nloc, nall, nloc], dtype=tf.int32),
32+
n_a_sel=1,
33+
n_r_sel=0,
34+
)
35+
36+
# Local and ghost gradients deliberately differ. The expected result
37+
# must use the ghost slice [4, 5, 6], not fold it onto [10, 11, 12].
38+
upstream = tf.constant(
39+
[
40+
[10.0, 11.0, 12.0, 4.0, 5.0, 6.0],
41+
[20.0, 21.0, 22.0, 1.0, 2.0, 3.0],
42+
],
43+
dtype=tf.float64,
44+
)
45+
grad_net = tf.gradients(force, net_deriv, grad_ys=upstream)[0]
46+
47+
with self.cached_session() as sess:
48+
actual_force, actual_grad = sess.run(
49+
[force, grad_net],
50+
feed_dict={
51+
net_deriv: [
52+
[1.0, 0.0, 0.0, 0.0],
53+
[2.0, 0.0, 0.0, 0.0],
54+
]
55+
},
56+
)
57+
58+
np.testing.assert_array_equal(
59+
actual_force,
60+
[
61+
[0.0, 0.0, 0.0, -1.0, -2.0, -3.0],
62+
[0.0, 0.0, 0.0, -2.0, -4.0, -6.0],
63+
],
64+
)
65+
np.testing.assert_array_equal(
66+
actual_grad,
67+
[[-32.0, 0.0, 0.0, 0.0], [-14.0, 0.0, 0.0, 0.0]],
68+
)
69+
70+
1171
class TestProdForceGrad(tf.test.TestCase):
1272
def setUp(self) -> None:
1373
self.sess = self.cached_session().__enter__()

0 commit comments

Comments
 (0)