Files
2026-07-13 12:40:42 +08:00

339 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
CPU unit tests for SetValueGradImpl.
Covers the four branches inside SetValueGradImpl
(paddle/phi/kernels/cpu/set_value_grad_kernel.cc):
Branch 1 - x_grad:
out_grad is copied to x_grad, then the slice region is zeroed out.
Branch 2 - value_grad (same shape, need_reverse=false):
When value_grad.dims() == out_dims and step > 0, the gradient is
extracted directly via stridedSlice.
Branch 3 - value_grad (same shape, need_reverse=true): ** previously uncovered **
When step < 0, reverse_vector is set and the stridedSlice result is
reversed before being written to value_grad.
Branch 4 - value_grad (broadcast/reduce):
When value_grad.dims() != out_dims, gradient is accumulated across
multiple broadcast tiles.
The test chain used throughout:
a = x * x
a[index] = value * value
loss = a.sum()
loss.backward()
Using v*v (instead of a direct assignment) makes gradients non-trivial and
easy to verify analytically:
d(loss)/d(x[i]) = 2*x[i] for i NOT in slice region; 0 inside region
d(loss)/d(v[i]) = 2*v[i] (each v[i] appears once as v[i]^2)
"""
import unittest
import numpy as np
import paddle
class TestSetValueGradXGrad(unittest.TestCase):
"""
Branch 1: x_grad path.
SetValueGradImpl copies out_grad into x_grad, then zeros the slice region.
Expected: x_grad[i] = 2*x[i] outside the slice; 0 inside.
"""
def setUp(self):
paddle.disable_static()
paddle.set_device('cpu')
def test_1d_slice_step1(self):
x_np = np.arange(1.0, 7.0, dtype='float32') # [1,2,3,4,5,6]
value_np = np.array([10.0, 10.0], dtype='float32')
x = paddle.to_tensor(x_np, stop_gradient=False)
value = paddle.to_tensor(value_np, stop_gradient=False)
a = x * x
a[1:3] = value # slice indices 1,2
loss = a.sum()
loss.backward()
expected = 2.0 * x_np
expected[1:3] = 0.0
np.testing.assert_allclose(
x.grad.numpy(),
expected,
rtol=1e-6,
err_msg=f'x_grad mismatch: expected {expected}, got {x.grad.numpy()}',
)
def test_2d_slice(self):
x_np = np.arange(1.0, 13.0, dtype='float32').reshape(3, 4)
value_np = np.ones((2, 4), dtype='float32') * 99.0
x = paddle.to_tensor(x_np, stop_gradient=False)
value = paddle.to_tensor(value_np, stop_gradient=False)
a = x * x
a[0:2, :] = value
loss = a.sum()
loss.backward()
expected = 2.0 * x_np
expected[0:2, :] = 0.0
np.testing.assert_allclose(
x.grad.numpy(),
expected,
rtol=1e-6,
)
class TestSetValueGradValueGradSameShape(unittest.TestCase):
"""
Branch 2: value_grad when value_grad.dims() == out_dims and step > 0.
SetValueGradImpl extracts the gradient via a single stridedSlice and assigns
it directly to value_grad (no accumulation, no reverse).
Expected: value_grad[i] = 2*v[i].
"""
def setUp(self):
paddle.disable_static()
paddle.set_device('cpu')
def test_1d_exact_shape(self):
x_np = np.arange(1.0, 7.0, dtype='float32')
value_np = np.array([10.0, 20.0, 30.0], dtype='float32')
x = paddle.to_tensor(x_np, stop_gradient=False)
value = paddle.to_tensor(value_np, stop_gradient=False)
a = x * x
a[2:5] = value * value # slice output shape [3] == value shape [3]
loss = a.sum()
loss.backward()
expected = 2.0 * value_np
np.testing.assert_allclose(
value.grad.numpy(),
expected,
rtol=1e-6,
)
def test_2d_exact_shape(self):
x_np = np.arange(1.0, 13.0, dtype='float32').reshape(4, 3)
value_np = np.arange(100.0, 106.0, dtype='float32').reshape(2, 3)
x = paddle.to_tensor(x_np, stop_gradient=False)
value = paddle.to_tensor(value_np, stop_gradient=False)
a = x * x
a[1:3, :] = (
value * value
) # slice output shape [2,3] == value shape [2,3]
loss = a.sum()
loss.backward()
expected = 2.0 * value_np
np.testing.assert_allclose(
value.grad.numpy(),
expected,
rtol=1e-5,
)
class TestSetValueGradNegativeStep(unittest.TestCase):
"""
Branch 3: need_reverse = true (step < 0).
When any axis has a negative step, reverse_vector is set for that axis and
SetValueGradImpl reverses the stridedSlice result before writing to
value_grad. This branch was NOT covered by existing tests.
The key invariant: value_grad[i] = 2*v[i] regardless of the direction in
which v was written into x, because the reverse restores the correspondence
between value positions and gradient positions.
"""
def setUp(self):
paddle.disable_static()
paddle.set_device('cpu')
def test_1d_step_neg1(self):
"""a[5:2:-1] = v => writes a[5],a[4],a[3]; step=-1 triggers need_reverse."""
x_np = np.arange(1.0, 7.0, dtype='float32') # [1,2,3,4,5,6]
value_np = np.array([10.0, 20.0, 30.0], dtype='float32')
x = paddle.to_tensor(x_np, stop_gradient=False)
value = paddle.to_tensor(value_np, stop_gradient=False)
a = x * x
a[5:2:-1] = value * value # fills indices 5,4,3
loss = a.sum()
loss.backward()
# x_grad: 2*x everywhere, 0 at indices {3,4,5}
expected_x_grad = 2.0 * x_np
expected_x_grad[3:6] = 0.0
# value_grad: 2*v (reverse restores original order)
expected_value_grad = 2.0 * value_np
np.testing.assert_allclose(
x.grad.numpy(),
expected_x_grad,
rtol=1e-6,
err_msg=f'x_grad: expected {expected_x_grad}, got {x.grad.numpy()}',
)
np.testing.assert_allclose(
value.grad.numpy(),
expected_value_grad,
rtol=1e-6,
err_msg=f'value_grad: expected {expected_value_grad}, got {value.grad.numpy()}',
)
def test_2d_negative_step_axis0(self):
"""a[3:0:-1, :] = v => fills rows 3,2,1 in reverse; step=-1 on axis 0."""
x_np = np.arange(1.0, 13.0, dtype='float32').reshape(4, 3)
value_np = np.arange(10.0, 19.0, dtype='float32').reshape(3, 3)
x = paddle.to_tensor(x_np, stop_gradient=False)
value = paddle.to_tensor(value_np, stop_gradient=False)
a = x * x
a[3:0:-1, :] = value * value # fills rows 3,2,1
loss = a.sum()
loss.backward()
# x_grad: 2*x for row 0 only; 0 for rows 1,2,3
expected_x_grad = 2.0 * x_np
expected_x_grad[1:4, :] = 0.0
expected_value_grad = 2.0 * value_np
np.testing.assert_allclose(
x.grad.numpy(),
expected_x_grad,
rtol=1e-6,
)
np.testing.assert_allclose(
value.grad.numpy(),
expected_value_grad,
rtol=1e-6,
)
def test_1d_step_neg2(self):
"""a[::-2] = v => step=-2, hits indices 7,5,3,1."""
x_np = np.arange(1.0, 9.0, dtype='float32') # [1..8], shape [8]
value_np = np.array([10.0, 20.0, 30.0, 40.0], dtype='float32')
x = paddle.to_tensor(x_np, stop_gradient=False)
value = paddle.to_tensor(value_np, stop_gradient=False)
a = x * x
a[::-2] = value * value # fills indices 7,5,3,1
loss = a.sum()
loss.backward()
# x_grad=0 at {1,3,5,7}; 2*x elsewhere
expected_x_grad = 2.0 * x_np
expected_x_grad[[1, 3, 5, 7]] = 0.0
expected_value_grad = 2.0 * value_np
np.testing.assert_allclose(
x.grad.numpy(),
expected_x_grad,
rtol=1e-6,
)
np.testing.assert_allclose(
value.grad.numpy(),
expected_value_grad,
rtol=1e-6,
)
class TestSetValueGradValueGradBroadcast(unittest.TestCase):
"""
Branch 4: value_grad when value_grad.dims() != out_dims (broadcast/reduce).
SetValueGradImpl accumulates the gradient from multiple broadcast tiles into
value_grad via repeated additions.
Expected: value_grad[i] = 2*v[i] * (number of tiles v[i] is broadcast into).
"""
def setUp(self):
paddle.disable_static()
paddle.set_device('cpu')
def test_scalar_broadcast_into_2d_slice(self):
"""value shape [1] broadcast into slice shape [2,4] (8 elements)."""
x_np = np.arange(1.0, 13.0, dtype='float32').reshape(3, 4)
value_np = np.array([5.0], dtype='float32')
x = paddle.to_tensor(x_np, stop_gradient=False)
value = paddle.to_tensor(value_np, stop_gradient=False)
a = x * x
a[0:2, :] = value * value # slice [2,4], value [1] -> 8 tiles
loss = a.sum()
loss.backward()
# d(loss)/d(v[0]) = sum over 8 positions of 2*v[0] = 2*5*8 = 80
expected_value_grad = np.array([2.0 * 5.0 * 8.0], dtype='float32')
np.testing.assert_allclose(
value.grad.numpy(),
expected_value_grad,
rtol=1e-5,
err_msg=f'Expected {expected_value_grad}, got {value.grad.numpy()}',
)
def test_row_vector_broadcast(self):
"""value shape [1,4] broadcast into slice shape [2,4] (2 rows)."""
x_np = np.arange(1.0, 13.0, dtype='float32').reshape(3, 4)
value_np = np.array([[10.0, 20.0, 30.0, 40.0]], dtype='float32')
x = paddle.to_tensor(x_np, stop_gradient=False)
value = paddle.to_tensor(value_np, stop_gradient=False)
a = x * x
a[0:2, :] = (
value * value
) # slice [2,4], value [1,4] -> each v[j] used 2×
loss = a.sum()
loss.backward()
# d(loss)/d(v[0,j]) = 2*v[0,j] * 2 (2 rows use the same v[0,j])
expected_value_grad = 2.0 * value_np * 2.0
np.testing.assert_allclose(
value.grad.numpy(),
expected_value_grad,
rtol=1e-5,
)
if __name__ == '__main__':
unittest.main()