701 lines
24 KiB
Python
701 lines
24 KiB
Python
# Copyright (c) 2025 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.
|
||
|
||
"""Test PyLayer tensor_hold_helper for _clear_dataptr protection.
|
||
|
||
Pipeline-parallel pattern:
|
||
1. outputs = Layer.apply(inputs) # forward: data is valid
|
||
2. loss = f(outputs) # build loss graph BEFORE clearing
|
||
3. outputs._clear_dataptr() # free activation memory
|
||
4. loss.backward() # backward via tensor_hold_helper recovery
|
||
|
||
tensor_hold_helper is a vector<shared_ptr<DenseTensor>> on PyLayerObject that
|
||
holds strong references to every DenseTensor impl saved via save_for_backward.
|
||
It is born with set_container (save_for_backward) and destroyed with the
|
||
PyLayerObject itself, preventing _clear_dataptr from freeing the underlying
|
||
allocation before backward runs.
|
||
"""
|
||
|
||
import gc
|
||
import unittest
|
||
|
||
import numpy as np
|
||
|
||
import paddle
|
||
from paddle.autograd import PyLayer
|
||
|
||
|
||
def _clear(tensors):
|
||
"""Call _clear_dataptr on a single tensor or iterable of tensors."""
|
||
if isinstance(tensors, (list, tuple)):
|
||
for t in tensors:
|
||
if hasattr(t, '_clear_dataptr'):
|
||
t._clear_dataptr()
|
||
elif hasattr(tensors, '_clear_dataptr'):
|
||
tensors._clear_dataptr()
|
||
|
||
|
||
class TestPyLayerClearDataptr(unittest.TestCase):
|
||
"""Core tests: _clear_dataptr on outputs does not break backward."""
|
||
|
||
def test_basic_clear_dataptr(self):
|
||
"""Single output, single saved tensor."""
|
||
|
||
class TanhLayer(PyLayer):
|
||
@staticmethod
|
||
def forward(ctx, x):
|
||
y = paddle.tanh(x)
|
||
ctx.save_for_backward(y)
|
||
return y
|
||
|
||
@staticmethod
|
||
def backward(ctx, dy):
|
||
(y,) = ctx.saved_tensor()
|
||
return dy * (1 - paddle.square(y))
|
||
|
||
x = paddle.randn([2, 3]).astype('float64')
|
||
x.stop_gradient = False
|
||
out = TanhLayer.apply(x)
|
||
loss = out.mean() # build graph first
|
||
_clear(out) # then free activation
|
||
loss.backward()
|
||
self.assertIsNotNone(x.grad)
|
||
|
||
def test_multiple_saved_tensors(self):
|
||
"""Multiple tensors passed to save_for_backward."""
|
||
|
||
class AddLayer(PyLayer):
|
||
@staticmethod
|
||
def forward(ctx, x, y):
|
||
ctx.save_for_backward(x, y)
|
||
return x + y
|
||
|
||
@staticmethod
|
||
def backward(ctx, dy):
|
||
x, y = ctx.saved_tensor()
|
||
return dy, dy
|
||
|
||
x = paddle.randn([2, 3]).astype('float64')
|
||
y = paddle.randn([2, 3]).astype('float64')
|
||
x.stop_gradient = False
|
||
y.stop_gradient = False
|
||
out = AddLayer.apply(x, y)
|
||
loss = out.mean()
|
||
_clear(out)
|
||
loss.backward()
|
||
self.assertIsNotNone(x.grad)
|
||
self.assertIsNotNone(y.grad)
|
||
|
||
def test_multiple_outputs(self):
|
||
"""Tuple output: both outputs are cleared."""
|
||
|
||
class MultiOutLayer(PyLayer):
|
||
@staticmethod
|
||
def forward(ctx, x):
|
||
y1 = paddle.tanh(x)
|
||
y2 = paddle.sin(x)
|
||
ctx.save_for_backward(y1, y2)
|
||
return y1, y2
|
||
|
||
@staticmethod
|
||
def backward(ctx, dy1, dy2):
|
||
y1, y2 = ctx.saved_tensor()
|
||
return dy1 * (1 - paddle.square(y1)) + dy2 * paddle.cos(y2)
|
||
|
||
x = paddle.randn([2, 3]).astype('float64')
|
||
x.stop_gradient = False
|
||
y1, y2 = MultiOutLayer.apply(x)
|
||
loss = (y1 + y2).mean() # build graph while data is valid
|
||
_clear([y1, y2])
|
||
loss.backward()
|
||
self.assertIsNotNone(x.grad)
|
||
|
||
def test_chained_computation(self):
|
||
"""Final output of a chain is cleared; intermediate kept for input."""
|
||
|
||
class TanhLayer(PyLayer):
|
||
@staticmethod
|
||
def forward(ctx, x):
|
||
y = paddle.tanh(x)
|
||
ctx.save_for_backward(y)
|
||
return y
|
||
|
||
@staticmethod
|
||
def backward(ctx, dy):
|
||
(y,) = ctx.saved_tensor()
|
||
return dy * (1 - paddle.square(y))
|
||
|
||
x = paddle.randn([2, 3]).astype('float64')
|
||
x.stop_gradient = False
|
||
y = TanhLayer.apply(x) # intermediate – not cleared
|
||
z = TanhLayer.apply(y) # final output
|
||
loss = z.mean()
|
||
_clear(z) # only clear final activation
|
||
loss.backward()
|
||
self.assertIsNotNone(x.grad)
|
||
|
||
def test_different_dtypes(self):
|
||
"""float32 / float64 (and float16 on GPU) all work after _clear_dataptr."""
|
||
|
||
class TanhLayer(PyLayer):
|
||
@staticmethod
|
||
def forward(ctx, x):
|
||
y = paddle.tanh(x)
|
||
ctx.save_for_backward(y)
|
||
return y
|
||
|
||
@staticmethod
|
||
def backward(ctx, dy):
|
||
(y,) = ctx.saved_tensor()
|
||
return dy * (1 - paddle.square(y))
|
||
|
||
dtypes = ['float32', 'float64']
|
||
if paddle.is_compiled_with_cuda():
|
||
dtypes.append('float16')
|
||
for dtype in dtypes:
|
||
x = paddle.randn([2, 3]).astype(dtype)
|
||
x.stop_gradient = False
|
||
out = TanhLayer.apply(x)
|
||
loss = out.mean()
|
||
_clear(out)
|
||
loss.backward()
|
||
self.assertIsNotNone(x.grad)
|
||
|
||
def test_memory_cleanup(self):
|
||
"""Multiple iterations: per-iteration objects are collectible."""
|
||
import weakref
|
||
|
||
class TanhLayer(PyLayer):
|
||
@staticmethod
|
||
def forward(ctx, x):
|
||
y = paddle.tanh(x)
|
||
ctx.save_for_backward(y)
|
||
return y
|
||
|
||
@staticmethod
|
||
def backward(ctx, dy):
|
||
(y,) = ctx.saved_tensor()
|
||
return dy * (1 - paddle.square(y))
|
||
|
||
# Track the first iteration's `out` via weakref; after the loop ends
|
||
# and gc runs, it must be collected. Catches holder leaks where
|
||
# tensor_hold_helper accidentally retains a strong reference across
|
||
# ctx lifetimes.
|
||
first_out_ref = None
|
||
for i in range(10):
|
||
x = paddle.randn([64, 64]).astype('float32')
|
||
x.stop_gradient = False
|
||
out = TanhLayer.apply(x)
|
||
if i == 0:
|
||
first_out_ref = weakref.ref(out)
|
||
loss = out.mean()
|
||
_clear(out)
|
||
loss.backward()
|
||
del x, out, loss
|
||
gc.collect()
|
||
self.assertIsNone(first_out_ref())
|
||
|
||
|
||
class TestCtxDirect(unittest.TestCase):
|
||
"""Unit tests for ctx API without going through PyLayer.apply().
|
||
|
||
These tests create a ctx object directly via cls._backward_function() and
|
||
exercise save_for_backward / saved_tensor / pop_saved_impl in isolation,
|
||
independently of the forward/backward dispatch machinery.
|
||
|
||
Key design: cls._backward_function is a subclass of PyLayerBackward which
|
||
inherits core.eager.PyLayer (C++ PyLayerObject). Instantiating it calls
|
||
PyLayerNew, giving a fully-initialized ctx with an empty tensor_hold_helper.
|
||
"""
|
||
|
||
def _make_ctx(self):
|
||
"""Create a bare ctx (PyLayerObject) without running forward."""
|
||
|
||
class _Stub(PyLayer):
|
||
@staticmethod
|
||
def forward(ctx, x):
|
||
return x
|
||
|
||
@staticmethod
|
||
def backward(ctx, dy):
|
||
return dy
|
||
|
||
return _Stub._backward_function()
|
||
|
||
# ------------------------------------------------------------------
|
||
# Basic save / recover
|
||
# ------------------------------------------------------------------
|
||
|
||
def test_direct_single_tensor_recover(self):
|
||
"""save_for_backward + _clear_dataptr + saved_tensor, no apply."""
|
||
ctx = self._make_ctx()
|
||
t = paddle.randn([2, 3]).astype('float64')
|
||
ctx.save_for_backward(t)
|
||
|
||
_clear(t)
|
||
|
||
(recovered,) = ctx.saved_tensor()
|
||
self.assertIsNotNone(recovered)
|
||
self.assertEqual(list(recovered.shape), [2, 3])
|
||
|
||
def test_direct_multiple_tensors_recover(self):
|
||
"""All tensors are recovered after _clear_dataptr, no apply."""
|
||
ctx = self._make_ctx()
|
||
a = paddle.randn([3]).astype('float32')
|
||
b = paddle.ones([4, 2]).astype('float64')
|
||
ctx.save_for_backward(a, b)
|
||
|
||
_clear(a)
|
||
_clear(b)
|
||
|
||
recovered = ctx.saved_tensor()
|
||
self.assertEqual(len(recovered), 2)
|
||
self.assertEqual(list(recovered[0].shape), [3])
|
||
self.assertEqual(list(recovered[1].shape), [4, 2])
|
||
|
||
def test_direct_no_clear(self):
|
||
"""saved_tensor returns correct values when _clear_dataptr was not called."""
|
||
ctx = self._make_ctx()
|
||
t = paddle.randn([2, 3]).astype('float32')
|
||
expected = t.numpy().copy()
|
||
ctx.save_for_backward(t)
|
||
|
||
(recovered,) = ctx.saved_tensor()
|
||
np.testing.assert_allclose(recovered.numpy(), expected, rtol=1e-6)
|
||
|
||
# ------------------------------------------------------------------
|
||
# pop_saved_impl
|
||
# ------------------------------------------------------------------
|
||
|
||
def test_pop_saved_impl_single(self):
|
||
"""pop_saved_impl removes the holder entry; recovered tensor stays valid."""
|
||
ctx = self._make_ctx()
|
||
t = paddle.randn([2, 3]).astype('float32')
|
||
orig = t.numpy().copy()
|
||
ctx.save_for_backward(t)
|
||
|
||
_clear(t)
|
||
(recovered,) = ctx.saved_tensor()
|
||
# Verify the recovered tensor carries the correct data (not just non-None).
|
||
np.testing.assert_allclose(recovered.numpy(), orig, rtol=1e-6)
|
||
|
||
# Pop removes the holder entry; recovered's own shared_ptr keeps data alive.
|
||
ctx._pop_saved_impl(recovered)
|
||
self.assertEqual(list(recovered.shape), [2, 3])
|
||
|
||
def test_pop_saved_impl_partial(self):
|
||
"""Pop both saved tensors one by one; proves each entry is stored independently."""
|
||
ctx = self._make_ctx()
|
||
a = paddle.randn([2]).astype('float32')
|
||
b = paddle.randn([3]).astype('float32')
|
||
ctx.save_for_backward(a, b)
|
||
|
||
_clear(a)
|
||
_clear(b)
|
||
recovered = ctx.saved_tensor()
|
||
self.assertEqual(len(recovered), 2)
|
||
|
||
# Pop the first entry; if holder only had one entry this would erase it
|
||
# and the second pop below would be a no-op instead of finding b's entry.
|
||
ctx._pop_saved_impl(recovered[0])
|
||
# Pop the second entry; succeeds only if b's entry is still in holder
|
||
# (i.e. the two entries are stored independently).
|
||
ctx._pop_saved_impl(recovered[1])
|
||
# Both recovered handles remain valid via their own shared_ptr copies.
|
||
self.assertEqual(list(recovered[0].shape), [2])
|
||
self.assertEqual(list(recovered[1].shape), [3])
|
||
|
||
def test_pop_saved_impl_no_clear(self):
|
||
"""pop_saved_impl does not crash when tensor was never cleared.
|
||
|
||
Also verifies the pop targets a specific entry: after popping t's
|
||
holder entry, a subsequent saved_tensor() call still succeeds and
|
||
returns t with its original data (pop did not corrupt container).
|
||
"""
|
||
ctx = self._make_ctx()
|
||
t = paddle.randn([5]).astype('float32')
|
||
orig = t.numpy().copy()
|
||
ctx.save_for_backward(t)
|
||
|
||
# No _clear_dataptr; pop should still succeed silently
|
||
ctx._pop_saved_impl(t)
|
||
# saved_tensor() must still return the tensor correctly.
|
||
(recovered,) = ctx.saved_tensor()
|
||
np.testing.assert_allclose(recovered.numpy(), orig, rtol=1e-6)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Deep-traversal via nested list in container
|
||
# ------------------------------------------------------------------
|
||
|
||
def test_nested_list_holder_populated(self):
|
||
"""Container with a nested list: CollectDenseTensors populates holder for all tensors.
|
||
|
||
save_for_backward packs args as a tuple, so the container at the top
|
||
level is always a tuple. But tuple *elements* may themselves be lists
|
||
(e.g. when a list is passed as one argument). CollectDenseTensors
|
||
recurses into them; verify via pop_saved_impl that both were collected.
|
||
"""
|
||
ctx = self._make_ctx()
|
||
t1 = paddle.randn([2]).astype('float32')
|
||
t2 = paddle.randn([3]).astype('float32')
|
||
|
||
# Directly assign a tuple whose sole element is a list of tensors.
|
||
# This bypasses save_for_backward's *args flattening so we can test
|
||
# the deep-traversal branch.
|
||
ctx.container = ([t1, t2],)
|
||
|
||
# Each pop finds and removes its entry; if CollectDenseTensors missed
|
||
# an entry, the corresponding pop is a silent no-op — so we follow
|
||
# each pair of pops with a redundant third pop that must also not crash,
|
||
# confirming the erase path is robust against missing entries.
|
||
ctx._pop_saved_impl(t1)
|
||
ctx._pop_saved_impl(t2)
|
||
ctx._pop_saved_impl(t1) # already removed — must be a silent no-op
|
||
|
||
def test_nested_tuple_holder_populated(self):
|
||
"""Container with a nested tuple: all inner tensors are held."""
|
||
ctx = self._make_ctx()
|
||
t1 = paddle.randn([2]).astype('float32')
|
||
t2 = paddle.randn([3]).astype('float32')
|
||
|
||
ctx.container = ((t1, t2),)
|
||
|
||
ctx._pop_saved_impl(t1)
|
||
ctx._pop_saved_impl(t2)
|
||
ctx._pop_saved_impl(t1) # already removed — must be a silent no-op
|
||
|
||
|
||
class TestCtxHoldRestore(unittest.TestCase):
|
||
"""Direct-ctx tests for _hold_tensors / _restore_held_tensors.
|
||
|
||
These cover the C++ WalkDenseTensors recursion (Tensor / tuple / list /
|
||
dict), the SavedTensorsHooks short-circuit in pylayer_hold_tensors, and
|
||
the ``impl() != nullptr`` early-return in pylayer_restore_held_tensors.
|
||
"""
|
||
|
||
def _make_ctx(self):
|
||
class _Stub(PyLayer):
|
||
@staticmethod
|
||
def forward(ctx, x):
|
||
return x
|
||
|
||
@staticmethod
|
||
def backward(ctx, dy):
|
||
return dy
|
||
|
||
return _Stub._backward_function()
|
||
|
||
def test_hold_restore_basic(self):
|
||
"""hold(tensor) + _clear_dataptr + restore re-installs impl_."""
|
||
ctx = self._make_ctx()
|
||
t = paddle.randn([2, 3]).astype('float32')
|
||
orig = t.numpy().copy()
|
||
|
||
ctx._hold_tensors(t)
|
||
_clear(t)
|
||
self.assertFalse(t._is_initialized())
|
||
|
||
ctx._restore_held_tensors()
|
||
self.assertTrue(t._is_initialized())
|
||
np.testing.assert_allclose(t.numpy(), orig, rtol=1e-6)
|
||
|
||
def test_hold_nested_containers(self):
|
||
"""tuple / list / dict values are all deep-traversed."""
|
||
ctx = self._make_ctx()
|
||
t_tuple = paddle.randn([2]).astype('float32')
|
||
t_list = paddle.randn([3]).astype('float32')
|
||
t_dict = paddle.randn([4]).astype('float32')
|
||
originals = [t.numpy().copy() for t in (t_tuple, t_list, t_dict)]
|
||
|
||
# One call with a container mixing all three Python collection types.
|
||
ctx._hold_tensors(((t_tuple,), [t_list], {'k': t_dict}))
|
||
|
||
_clear([t_tuple, t_list, t_dict])
|
||
ctx._restore_held_tensors()
|
||
|
||
for got, orig in zip((t_tuple, t_list, t_dict), originals):
|
||
self.assertTrue(got._is_initialized())
|
||
np.testing.assert_allclose(got.numpy(), orig, rtol=1e-6)
|
||
|
||
def test_hold_none_is_noop(self):
|
||
"""_hold_tensors(None) collects nothing; restore is a no-op."""
|
||
ctx = self._make_ctx()
|
||
ctx._hold_tensors(None)
|
||
ctx._restore_held_tensors() # must not crash
|
||
|
||
def test_hold_scalar_top_level_noop(self):
|
||
"""_hold_tensors on a bare non-container scalar collects nothing."""
|
||
ctx = self._make_ctx()
|
||
for val in (42, 3.14, "str", b"bytes"):
|
||
ctx._hold_tensors(val)
|
||
ctx._restore_held_tensors() # must not crash
|
||
|
||
def test_restore_skips_valid_impl(self):
|
||
"""Restore leaves tensors whose impl is still valid untouched."""
|
||
ctx = self._make_ctx()
|
||
t_cleared = paddle.randn([2]).astype('float32')
|
||
t_kept = paddle.randn([3]).astype('float32')
|
||
orig_cleared = t_cleared.numpy().copy()
|
||
orig_kept = t_kept.numpy().copy()
|
||
|
||
ctx._hold_tensors([t_cleared, t_kept])
|
||
_clear(t_cleared) # only one is cleared
|
||
ctx._restore_held_tensors()
|
||
|
||
# cleared tensor resurrected
|
||
np.testing.assert_allclose(t_cleared.numpy(), orig_cleared, rtol=1e-6)
|
||
# kept tensor's impl untouched — covers the ``if (!tensor.impl())``
|
||
# false branch in pylayer_restore_held_tensors.
|
||
self.assertTrue(t_kept._is_initialized())
|
||
np.testing.assert_allclose(t_kept.numpy(), orig_kept, rtol=1e-6)
|
||
|
||
def test_hold_non_tensor_leaves_ignored(self):
|
||
"""Non-Tensor leaves (int/float/str/None/bytes) are silently skipped."""
|
||
ctx = self._make_ctx()
|
||
t1 = paddle.randn([2]).astype('float32')
|
||
t2 = paddle.randn([3]).astype('float32')
|
||
orig1 = t1.numpy().copy()
|
||
orig2 = t2.numpy().copy()
|
||
|
||
# Container mixes Tensors with int / float / str / None / bytes /
|
||
# a dict whose values are non-Tensor; WalkDenseTensors must descend
|
||
# into the containers, collect t1 / t2, and ignore everything else.
|
||
mixed = (
|
||
t1,
|
||
42,
|
||
"hello",
|
||
None,
|
||
[3.14, t2, b"bytes"],
|
||
{'tag': 'x', 'n': 7, 'nested': (None, 'str')},
|
||
)
|
||
ctx._hold_tensors(mixed)
|
||
|
||
_clear([t1, t2])
|
||
ctx._restore_held_tensors()
|
||
|
||
np.testing.assert_allclose(t1.numpy(), orig1, rtol=1e-6)
|
||
np.testing.assert_allclose(t2.numpy(), orig2, rtol=1e-6)
|
||
|
||
def test_hold_skipped_under_saved_tensors_hooks(self):
|
||
"""When saved_tensors_hooks is enabled _hold_tensors collects nothing."""
|
||
ctx = self._make_ctx()
|
||
t = paddle.randn([2, 3]).astype('float32')
|
||
|
||
with paddle.autograd.saved_tensors_hooks(lambda x: x, lambda x: x):
|
||
ctx._hold_tensors(t)
|
||
|
||
_clear(t)
|
||
ctx._restore_held_tensors()
|
||
# holder was not populated, so impl stays empty after _clear_dataptr.
|
||
self.assertFalse(t._is_initialized())
|
||
|
||
|
||
class TestRecomputeClosureHold(unittest.TestCase):
|
||
"""End-to-end recompute coverage of the Python-side closure helper.
|
||
|
||
Covers ``_closure_cell_values`` (plain fn / nn.Layer / no-closure) and the
|
||
``_has_held_tensors`` True/False branches in RecomputeFunction.
|
||
"""
|
||
|
||
def setUp(self):
|
||
np.random.seed(1234)
|
||
paddle.seed(1234)
|
||
|
||
@staticmethod
|
||
def _clone_leaf(t):
|
||
out = paddle.to_tensor(t.numpy(), dtype=t.dtype)
|
||
out.stop_gradient = False
|
||
return out
|
||
|
||
def test_closure_cell_values_empty_cell(self):
|
||
"""Empty cell triggers ValueError branch; valid cells still collected."""
|
||
from paddle.distributed.fleet.recompute.recompute import (
|
||
_closure_cell_values,
|
||
)
|
||
|
||
def outer():
|
||
x = 1 # will be deleted → empty cell
|
||
y = paddle.randn([2])
|
||
|
||
def inner(a):
|
||
return a + x + y # noqa: F821
|
||
|
||
del x
|
||
return inner, y
|
||
|
||
fn, y = outer()
|
||
vals = _closure_cell_values(fn)
|
||
# Empty cell dropped by the ValueError branch; only y remains.
|
||
self.assertEqual(vals, (y,))
|
||
|
||
def test_recompute_no_closure(self):
|
||
"""run_fn has no __closure__: _has_held_tensors=False, restore skipped."""
|
||
from paddle.distributed.fleet.utils import recompute
|
||
|
||
def run_fn(a, b):
|
||
return (a * b + a).sum()
|
||
|
||
a = paddle.randn([4, 4])
|
||
a.stop_gradient = False
|
||
b = paddle.randn([4, 4])
|
||
b.stop_gradient = False
|
||
a_ref = self._clone_leaf(a)
|
||
b_ref = self._clone_leaf(b)
|
||
|
||
loss = recompute(run_fn, a, b)
|
||
_clear([a, b])
|
||
# Sanity: _clear actually nulled impls — otherwise "restore succeeded"
|
||
# would be trivially true and mask regressions.
|
||
self.assertFalse(a._is_initialized())
|
||
self.assertFalse(b._is_initialized())
|
||
loss.backward()
|
||
run_fn(a_ref, b_ref).backward()
|
||
|
||
np.testing.assert_allclose(
|
||
a.grad.numpy(), a_ref.grad.numpy(), rtol=1e-4
|
||
)
|
||
np.testing.assert_allclose(
|
||
b.grad.numpy(), b_ref.grad.numpy(), rtol=1e-4
|
||
)
|
||
|
||
def test_recompute_closure_tensors(self):
|
||
"""Closure captures Tensor / tuple / list / dict: all restored."""
|
||
from paddle.distributed.fleet.utils import recompute
|
||
|
||
w_s = paddle.randn([4, 4])
|
||
w_s.stop_gradient = False
|
||
w_a = paddle.randn([4, 4])
|
||
w_a.stop_gradient = False
|
||
w_b = paddle.randn([4, 4])
|
||
w_b.stop_gradient = False
|
||
w_d = paddle.randn([4, 4])
|
||
w_d.stop_gradient = False
|
||
refs = [self._clone_leaf(t) for t in (w_s, w_a, w_b, w_d)]
|
||
|
||
def make_fn(s, pair, mapping):
|
||
def fn(x):
|
||
a, b = pair
|
||
return (x @ s + a * x + b * x + mapping['k'] * x).sum()
|
||
|
||
return fn
|
||
|
||
x = paddle.randn([4, 4])
|
||
x.stop_gradient = False
|
||
x_ref = self._clone_leaf(x)
|
||
|
||
run_fn = make_fn(w_s, (w_a, w_b), {'k': w_d})
|
||
ref_fn = make_fn(refs[0], (refs[1], refs[2]), {'k': refs[3]})
|
||
|
||
loss = recompute(run_fn, x)
|
||
_clear([x, w_s, w_a, w_b, w_d])
|
||
for t in (x, w_s, w_a, w_b, w_d):
|
||
self.assertFalse(t._is_initialized())
|
||
loss.backward()
|
||
ref_fn(x_ref).backward()
|
||
|
||
for got, expect in zip((x, w_s, w_a, w_b, w_d), (x_ref, *refs)):
|
||
self.assertIsNotNone(got.grad)
|
||
np.testing.assert_allclose(
|
||
got.grad.numpy(), expect.grad.numpy(), rtol=1e-4
|
||
)
|
||
|
||
def test_recompute_all_grad_from_closure(self):
|
||
"""Trainable tensors captured via closure must receive grads.
|
||
|
||
Real-world pattern: trainable weights are closure-captured while the
|
||
PyLayer arg is a regular activation. Verifies that closure-captured
|
||
``w1`` / ``w2`` tensors are held across ``_clear_dataptr()`` and their
|
||
grads are computed correctly during the recomputed backward.
|
||
"""
|
||
from paddle.distributed.fleet.utils import recompute
|
||
|
||
w1 = paddle.randn([4, 4])
|
||
w1.stop_gradient = False
|
||
w2 = paddle.randn([4, 4])
|
||
w2.stop_gradient = False
|
||
w1_ref = self._clone_leaf(w1)
|
||
w2_ref = self._clone_leaf(w2)
|
||
|
||
def make_fn(a, b):
|
||
def fn(inp):
|
||
return (inp * a * b).sum()
|
||
|
||
return fn
|
||
|
||
run_fn = make_fn(w1, w2)
|
||
ref_fn = make_fn(w1_ref, w2_ref)
|
||
|
||
inp = paddle.ones([4, 4])
|
||
inp.stop_gradient = False
|
||
inp_ref = paddle.ones([4, 4])
|
||
inp_ref.stop_gradient = False
|
||
|
||
loss = recompute(run_fn, inp)
|
||
_clear([inp, w1, w2])
|
||
for t in (inp, w1, w2):
|
||
self.assertFalse(t._is_initialized())
|
||
loss.backward()
|
||
ref_fn(inp_ref).backward()
|
||
|
||
np.testing.assert_allclose(
|
||
w1.grad.numpy(), w1_ref.grad.numpy(), rtol=1e-4
|
||
)
|
||
np.testing.assert_allclose(
|
||
w2.grad.numpy(), w2_ref.grad.numpy(), rtol=1e-4
|
||
)
|
||
|
||
def test_recompute_layer_forward_closure(self):
|
||
"""paddle.nn.Layer branch of _closure_cell_values."""
|
||
from paddle.distributed.fleet.utils import recompute
|
||
|
||
bias = paddle.randn([4, 4])
|
||
bias.stop_gradient = False
|
||
bias_ref = self._clone_leaf(bias)
|
||
|
||
class MyLayer(paddle.nn.Layer):
|
||
def __init__(self, captured):
|
||
super().__init__()
|
||
|
||
def forward(x):
|
||
return (x + captured).sum()
|
||
|
||
self.forward = forward
|
||
|
||
def forward(self, x): # pragma: no cover
|
||
raise RuntimeError
|
||
|
||
layer = MyLayer(bias)
|
||
layer_ref = MyLayer(bias_ref)
|
||
x = paddle.randn([4, 4])
|
||
x.stop_gradient = False
|
||
x_ref = self._clone_leaf(x)
|
||
|
||
loss = recompute(layer, x)
|
||
_clear([x, bias])
|
||
self.assertFalse(x._is_initialized())
|
||
self.assertFalse(bias._is_initialized())
|
||
loss.backward()
|
||
layer_ref(x_ref).backward()
|
||
|
||
np.testing.assert_allclose(
|
||
x.grad.numpy(), x_ref.grad.numpy(), rtol=1e-4
|
||
)
|
||
np.testing.assert_allclose(
|
||
bias.grad.numpy(), bias_ref.grad.numpy(), rtol=1e-4
|
||
)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
unittest.main()
|