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

397 lines
15 KiB
Python

# 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.
import unittest
import paddle
from paddle.distributed.fleet.meta_parallel.pp_utils.forward_backward_overlap_utils import (
ScheduleNode,
clone_and_clear_dataptr,
detach_and_requires_grad,
)
def simple_forward_func(inputs):
return inputs * 2
def forward_func_with_labels(inputs, labels):
return (inputs * 2, labels * 3)
class TestScheduleNode(unittest.TestCase):
def test_init(self):
"""Test ScheduleNode initialization."""
node = ScheduleNode(simple_forward_func, name="test_node")
self.assertEqual(node.name, "test_node")
self.assertEqual(node.fwd_func, simple_forward_func)
self.assertIsNone(node.inputs)
self.assertIsNone(node.outputs)
self.assertIsNone(node.labels)
self.assertIsNone(node.scale_loss_factor)
def test_init_default_name(self):
"""Test ScheduleNode initialization with default name."""
node = ScheduleNode(simple_forward_func)
self.assertEqual(node.name, "")
def test_forward_basic(self):
"""Test forward pass with basic inputs."""
node = ScheduleNode(simple_forward_func)
inputs = paddle.randn([2, 3])
outputs = node.forward(inputs)
self.assertIsNotNone(node.inputs)
self.assertIsNotNone(node.outputs)
self.assertEqual(outputs.shape, inputs.shape)
self.assertTrue(paddle.allclose(outputs, inputs * 2))
def test_forward_with_tuple_inputs(self):
"""Test forward pass with tuple inputs."""
node = ScheduleNode(lambda inputs: (inputs[0] * 2, inputs[1] * 3))
input1 = paddle.randn([2, 3])
input2 = paddle.randn([2, 3])
outputs = node.forward((input1, input2))
self.assertIsInstance(outputs, tuple)
self.assertEqual(len(outputs), 2)
def test_forward_with_kwargs(self):
"""Test forward pass with keyword arguments."""
node = ScheduleNode(lambda x, factor=1: x * factor)
inputs = paddle.randn([2, 3])
outputs = node.forward(inputs, factor=3)
self.assertTrue(paddle.allclose(outputs, inputs * 3))
def test_forward_with_labels(self):
"""Test forward pass with labels set."""
node = ScheduleNode(forward_func_with_labels, name="test_node")
node.labels = paddle.ones([2, 3])
inputs = paddle.randn([2, 3])
outputs = node.forward(inputs)
self.assertIsNotNone(node.outputs)
def test_forward_with_scale_loss_factor(self):
"""Test forward pass with scale_loss_factor."""
node = ScheduleNode(simple_forward_func)
node.scale_loss_factor = 2.0
inputs = paddle.randn([2, 3])
outputs = node.forward(inputs)
# Output should be scaled by 1/scale_loss_factor
self.assertTrue(paddle.allclose(outputs, inputs))
def test_backward_basic(self):
"""Test backward pass."""
node = ScheduleNode(simple_forward_func)
inputs = paddle.randn([2, 3])
inputs.stop_gradient = False
outputs = node.forward(inputs)
grads = node.backward()
# Check that gradients are returned
self.assertIsInstance(grads, tuple)
def test_backward_with_scaler(self):
"""Test backward pass with scaler."""
node = ScheduleNode(simple_forward_func)
inputs = paddle.randn([2, 3])
inputs.stop_gradient = False
node.forward(inputs)
scaler = paddle.amp.GradScaler(init_loss_scaling=2.0)
grads = node.backward(scaler=scaler)
self.assertIsInstance(grads, tuple)
def test_backward_with_output_grad(self):
"""Test backward pass with provided output gradients."""
node = ScheduleNode(simple_forward_func)
inputs = paddle.randn([2, 3])
inputs.stop_gradient = False
node.forward(inputs)
output_grad = paddle.ones([2, 3])
grads = node.backward(output_grad=output_grad)
self.assertIsInstance(grads, tuple)
def test_reset_states(self):
"""Test _reset_states method."""
node = ScheduleNode(simple_forward_func)
node.labels = paddle.ones([2, 3])
node.scale_loss_factor = 2.0
node._reset_states()
self.assertIsNone(node.inputs)
self.assertIsNone(node.outputs)
self.assertIsNone(node.labels)
self.assertIsNone(node.scale_loss_factor)
class TestDetachAndRequiresGrad(unittest.TestCase):
def test_detach_single_tensor(self):
"""Test detach_and_requires_grad with a single tensor."""
tensor = paddle.randn([2, 3])
tensor.stop_gradient = False
result = detach_and_requires_grad(tensor)
self.assertIsInstance(result, paddle.Tensor)
self.assertFalse(result.stop_gradient)
def test_detach_tensor_with_stop_gradient_true(self):
"""Test detach_and_requires_grad with stop_gradient=True."""
tensor = paddle.randn([2, 3])
tensor.stop_gradient = True
result = detach_and_requires_grad(tensor)
self.assertTrue(result.stop_gradient)
def test_detach_list_of_tensors(self):
"""Test detach_and_requires_grad with a list of tensors."""
tensor1 = paddle.randn([2, 3])
tensor1.stop_gradient = False
tensor2 = paddle.randn([2, 3])
tensor2.stop_gradient = True
inputs = [tensor1, tensor2]
result = detach_and_requires_grad(inputs)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 2)
self.assertFalse(result[0].stop_gradient)
self.assertTrue(result[1].stop_gradient)
def test_detach_tuple_of_tensors(self):
"""Test detach_and_requires_grad with a tuple of tensors."""
tensor1 = paddle.randn([2, 3])
tensor1.stop_gradient = False
tensor2 = paddle.randn([2, 3])
tensor2.stop_gradient = False
inputs = (tensor1, tensor2)
result = detach_and_requires_grad(inputs)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 2)
self.assertFalse(result[0].stop_gradient)
self.assertFalse(result[1].stop_gradient)
def test_detach_nested_tuple(self):
"""Test detach_and_requires_grad with nested tuple."""
tensor1 = paddle.randn([2, 3])
tensor1.stop_gradient = False
tensor2 = paddle.randn([2, 3])
tensor2.stop_gradient = True
inputs = ((tensor1,), (tensor2,))
result = detach_and_requires_grad(inputs)
self.assertIsInstance(result, tuple)
self.assertIsInstance(result[0], tuple)
self.assertFalse(result[0][0].stop_gradient)
self.assertTrue(result[1][0].stop_gradient)
def test_detach_nested_list(self):
"""Test detach_and_requires_grad with nested list."""
tensor1 = paddle.randn([2, 3])
tensor1.stop_gradient = False
tensor2 = paddle.randn([2, 3])
tensor2.stop_gradient = True
inputs = [[tensor1], [tensor2]]
result = detach_and_requires_grad(inputs)
self.assertIsInstance(result, list)
self.assertIsInstance(result[0], list)
self.assertFalse(result[0][0].stop_gradient)
self.assertTrue(result[1][0].stop_gradient)
def test_detach_list_with_none(self):
"""Test detach_and_requires_grad with list containing None."""
tensor = paddle.randn([2, 3])
tensor.stop_gradient = False
inputs = [tensor, None, "string_value", 123]
result = detach_and_requires_grad(inputs)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 4)
self.assertFalse(result[0].stop_gradient)
self.assertIsNone(result[1])
self.assertEqual(result[2], "string_value")
self.assertEqual(result[3], 123)
def test_detach_tuple_with_none(self):
"""Test detach_and_requires_grad with tuple containing None."""
tensor = paddle.randn([2, 3])
tensor.stop_gradient = False
inputs = (tensor, None, "string")
result = detach_and_requires_grad(inputs)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 3)
self.assertFalse(result[0].stop_gradient)
self.assertIsNone(result[1])
self.assertEqual(result[2], "string")
def test_detach_dict(self):
"""Test detach_and_requires_grad with dict."""
tensor1 = paddle.randn([2, 3])
tensor1.stop_gradient = False
tensor2 = paddle.randn([2, 3])
tensor2.stop_gradient = True
inputs = {"key1": tensor1, "key2": tensor2}
result = detach_and_requires_grad(inputs)
self.assertIsInstance(result, dict)
self.assertEqual(len(result), 2)
self.assertFalse(result["key1"].stop_gradient)
self.assertTrue(result["key2"].stop_gradient)
def test_detach_dict_with_none(self):
"""Test detach_and_requires_grad with dict containing None."""
tensor = paddle.randn([2, 3])
tensor.stop_gradient = False
inputs = {"key1": tensor, "key2": None}
result = detach_and_requires_grad(inputs)
self.assertIsInstance(result, dict)
self.assertEqual(len(result), 2)
self.assertFalse(result["key1"].stop_gradient)
self.assertIsNone(result["key2"])
class TestCloneAndClearDataptr(unittest.TestCase):
def test_clone_single_tensor(self):
"""Test clone_and_clear_dataptr with a single tensor."""
tensor = paddle.randn([2, 3])
result = clone_and_clear_dataptr(tensor, clear_dataptr=False)
self.assertIsInstance(result, paddle.Tensor)
self.assertEqual(result.shape, tensor.shape)
def test_clone_single_tensor_clear_dataptr(self):
"""Test clone_and_clear_dataptr with clear_dataptr=True."""
tensor = paddle.randn([2, 3])
result = clone_and_clear_dataptr(tensor, clear_dataptr=True)
self.assertIsInstance(result, paddle.Tensor)
# After _clear_dataptr(), the shape may be cleared
def test_clone_list_of_tensors(self):
"""Test clone_and_clear_dataptr with a list of tensors."""
tensor1 = paddle.randn([2, 3])
tensor2 = paddle.randn([2, 3])
outputs = [tensor1, tensor2]
result = clone_and_clear_dataptr(outputs, clear_dataptr=False)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 2)
self.assertEqual(result[0].shape, tensor1.shape)
self.assertEqual(result[1].shape, tensor2.shape)
def test_clone_list_clear_dataptr(self):
"""Test clone_and_clear_dataptr with list and clear_dataptr=True."""
tensor1 = paddle.randn([2, 3])
tensor2 = paddle.randn([2, 3])
outputs = [tensor1, tensor2]
result = clone_and_clear_dataptr(outputs, clear_dataptr=True)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 2)
def test_clone_tuple_of_tensors(self):
"""Test clone_and_clear_dataptr with a tuple of tensors."""
tensor1 = paddle.randn([2, 3])
tensor2 = paddle.randn([2, 3])
outputs = (tensor1, tensor2)
result = clone_and_clear_dataptr(outputs, clear_dataptr=False)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 2)
self.assertEqual(result[0].shape, tensor1.shape)
self.assertEqual(result[1].shape, tensor2.shape)
def test_clone_tuple_clear_dataptr(self):
"""Test clone_and_clear_dataptr with tuple and clear_dataptr=True."""
tensor1 = paddle.randn([2, 3])
tensor2 = paddle.randn([2, 3])
outputs = (tensor1, tensor2)
result = clone_and_clear_dataptr(outputs, clear_dataptr=True)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 2)
def test_clone_list_with_none(self):
"""Test clone_and_clear_dataptr with list containing None and non-tensors."""
tensor1 = paddle.randn([2, 3])
tensor2 = paddle.randn([2, 3])
outputs = [tensor1, None, "string", tensor2, 123]
result = clone_and_clear_dataptr(outputs, clear_dataptr=False)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 2) # Only tensors are included
self.assertEqual(result[0].shape, tensor1.shape)
self.assertEqual(result[1].shape, tensor2.shape)
def test_clone_tuple_with_none(self):
"""Test clone_and_clear_dataptr with tuple containing None and non-tensors."""
tensor1 = paddle.randn([2, 3])
tensor2 = paddle.randn([2, 3])
outputs = (tensor1, None, "string", tensor2)
result = clone_and_clear_dataptr(outputs, clear_dataptr=False)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 2) # Only tensors are included
self.assertEqual(result[0].shape, tensor1.shape)
self.assertEqual(result[1].shape, tensor2.shape)
def test_clone_dict(self):
"""Test clone_and_clear_dataptr with dict."""
tensor1 = paddle.randn([2, 3])
tensor2 = paddle.randn([2, 3])
outputs = {"key1": tensor1, "key2": tensor2}
result = clone_and_clear_dataptr(outputs, clear_dataptr=False)
self.assertIsInstance(result, dict)
self.assertEqual(len(result), 2)
self.assertIn("key1", result)
self.assertIn("key2", result)
self.assertEqual(result["key1"].shape, tensor1.shape)
self.assertEqual(result["key2"].shape, tensor2.shape)
def test_clone_dict_clear_dataptr(self):
"""Test clone_and_clear_dataptr with dict and clear_dataptr=True."""
tensor1 = paddle.randn([2, 3])
tensor2 = paddle.randn([2, 3])
outputs = {"key1": tensor1, "key2": tensor2}
result = clone_and_clear_dataptr(outputs, clear_dataptr=True)
self.assertIsInstance(result, dict)
self.assertEqual(len(result), 2)
def test_clone_dict_with_none(self):
"""Test clone_and_clear_dataptr with dict containing None and non-tensors."""
tensor = paddle.randn([2, 3])
outputs = {"key1": tensor, "key2": None, "key3": "value"}
result = clone_and_clear_dataptr(outputs, clear_dataptr=False)
self.assertIsInstance(result, dict)
self.assertEqual(len(result), 1) # Only tensors are included
self.assertIn("key1", result)
self.assertEqual(result["key1"].shape, tensor.shape)
if __name__ == "__main__":
unittest.main()