paddlepaddle--paddle
701 行
24 KiB
Python
701 行
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()
|