397 lines
15 KiB
Python
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()
|