chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:42 +08:00
commit e25996e7db
15472 changed files with 3536181 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
# Copyright (c) 2021 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.
from ..base.dygraph.base import ( # noqa: F401
enable_grad,
grad,
inference_mode,
is_grad_enabled,
no_grad_ as no_grad,
set_grad_enabled,
)
from . import ( # noqa: F401
backward_mode,
function,
grad_mode,
ir_backward,
)
from .autograd import hessian, jacobian
from .backward_mode import backward
from .py_layer import PyLayer, PyLayerContext
from .saved_tensors_hooks import saved_tensors_hooks
Function = PyLayer
__all__ = [
'jacobian',
'hessian',
'backward',
'PyLayer',
'Function',
'PyLayerContext',
'saved_tensors_hooks',
]
+775
View File
@@ -0,0 +1,775 @@
# Copyright (c) 2023 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.
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING, overload
import paddle
from paddle.base import framework
if TYPE_CHECKING:
from paddle import Tensor
def as_tensors(xs):
if isinstance(xs, framework.Variable):
return xs
elif isinstance(xs, Sequence):
return tuple(xs)
else:
return xs
class Jacobian:
r"""Computes the Jacobian matrix of given xs and ys.
Once the Jacobian ``J`` is constructed, you can use a multidimensional index
to retrieve the submatrix of ``J``, as same as slicing a Tensor. The
submatrix is lazily evaluated along row axis, and will be cached once
evaluated.
you can retrieve the submatrix by
following methods:
* J[:], retrieving the full matrix.
* J[:, :, j], retrieving the partial derivatives w.r.t. the j'th input
variable.
* J[:, i, :], retrieving the partial derivatives w.r.t. the i'th output
variable.
* J[:, i, j], retrieving the partial derivatives w.r.t. the i'th output
variable and the j'th input variable.
Notes:
Ellipsis index is not supported currently.
Args:
ys (Tensor|Tuple[Tensor, ...]): The output derived from xs .
xs (Tensor|Tuple[Tensor, ...]): The input tensor(s) .
is_batched (bool): If true, the first axis is batch axis. Defaults to
False.
Returns:
Jacobian (Object): A python object retains the Jacobian matrix.
"""
def __init__(
self,
ys: Tensor,
xs: Tensor,
is_batched: bool = False,
) -> None:
if not is_batched:
if not 0 <= len(xs.shape) <= 1:
raise ValueError(
f"xs.ndim should be 0 or 1 when is_batched=False"
f" but got {len(xs.shape)}"
)
if not 0 <= len(ys.shape) <= 1:
raise ValueError(
f"ys.ndim should be 0 or 1 when is_batched=False"
f" but got {len(ys.shape)}"
)
self._jacobian = _JacobianNoBatch(ys, xs)
else:
if not 1 <= len(ys.shape) <= 2:
raise ValueError(
f"ys.ndim should be 1 or 2 when is_batched=True"
f" but got {len(ys.shape)}"
)
if not 1 <= len(xs.shape) <= 2:
raise ValueError(
f"xs.ndim should be 1 or 2 when is_batched=True"
f" but got {len(xs.shape)}"
)
self._jacobian = _JacobianBatchFirst(ys, xs)
@property
def shape(self) -> list[int]:
"""The shape of flattened Jacobian matrix."""
return self._jacobian.shape
def __getitem__(self, indexes):
return self._jacobian[indexes]
def __getattr__(self, __name: str): # noqa: PYI063
if __name == "shape":
return getattr(self._jacobian, __name)
if __name == "_evaluate_all":
return getattr(self._jacobian, __name)
return getattr(self._jacobian._evaluate_all(), __name)
def __add__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs + rhs
def __sub__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs - rhs
def __mul__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs * rhs
def __div__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs / rhs
def __truediv__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs / rhs
def __pow__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs**rhs
def __mod__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs % rhs
def __floordiv__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs // rhs
def __matmul__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs @ rhs
def __eq__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs == rhs
def __ne__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs != rhs
def __lt__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs < rhs
def __le__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs <= rhs
def __gt__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs > rhs
def __ge__(self, other):
lhs = self._evaluate_all()
rhs = other._evaluate_all() if isinstance(other, Jacobian) else other
return lhs >= rhs
class Hessian(Jacobian):
pass
class _Jacobian:
"""The base class for computing Jacobian matrix.
``_Jacobian`` implements the core logic of multidimensional index and lazy
evaluation for Jacobian matrix, subclass only need to overwrite following
methods:
* ``_lazy_axis()``, return the axis along which will be lazy
evaluating.
* ``_flatten(xs)``, flattens the inputs ``xs``.
* ``_evaluate(index)``, evaluates one slice along ``_lazy_axis`` .
Notes:
Because currently PaddlePaddle only support reverse differentiation by
``paddle.grad``, so lazy evaluation is only supported along the row of
Jacobian matrix, which means that slicing along row will get better
performance.
"""
def __init__(self, ys, xs):
self.original_xs_shape = xs.shape
self.original_ys_shape = ys.shape
self._xs = xs
self._ys = ys
if len(self._ys.shape) == 0 and not self.is_batched:
self._ys = self._ys.reshape(
[
-1,
]
)
if len(self._ys.shape) == 1 and self.is_batched:
self._ys = self._ys.reshape([-1, 1])
self._flatten_xs = self._flatten(as_tensors(self._xs))
self._flatten_ys = self._flatten(as_tensors(self._ys))
self._cache = {}
@property
def _lazy_axis(self):
""" "The axis of lazily evaluated."""
raise NotImplementedError
def _lazy_indexes(self, indexes):
idx = indexes[self._lazy_axis]
return (
(idx,)
if isinstance(idx, int)
else tuple(range(idx.start, idx.stop, idx.step))
)
def _flatten(self, xs):
raise NotImplementedError
def _shifted_indexes(self, indexes, lazy_axis_size=0):
idx = indexes[self._lazy_axis]
shifted_lazy_axis_idx = (
0 if isinstance(idx, int) else slice(0, lazy_axis_size, 1)
)
return (
*indexes[: self._lazy_axis],
shifted_lazy_axis_idx,
*indexes[self._lazy_axis + 1 :],
)
def __getitem__(self, indexes):
if self.is_batched is False:
if len(self.shape) == 0:
# xs and ys are both 0-D tensor
raise IndexError("0-D tensor can not be indexed.")
elif len(self.shape) == 1:
# either ys or xs is 0-D tensor
indexes = (
(0, indexes)
if len(self.original_ys_shape) == 0
else (indexes, 0)
)
else:
if len(self.shape) == 1:
# xs and ys are both 1-D tensor
indexes = (indexes, 0, 0)
elif len(self.shape) == 2:
# either xs or ys is 1-D tensor
if isinstance(indexes, slice):
indexes = (indexes, slice(None, None, None))
else:
indexes = (
(indexes[0], 0, indexes[1])
if len(self.original_ys_shape) == 1
else (indexes[0], indexes[1], 0)
)
indexes = _multi_index(indexes, self.inner_shape)
if isinstance(indexes[self._lazy_axis], int):
other_indexes = (
indexes[: self._lazy_axis] + indexes[self._lazy_axis + 1 :]
)
return self._cached_evaluate(indexes[self._lazy_axis])[
other_indexes
]
lazy_indexes = self._lazy_indexes(indexes)
# Using concat and reshape to replace stack operator temporarily, as
# it is not a primitive operator.
shape = list(self.inner_shape)
shape[self._lazy_axis] = len(lazy_indexes)
part_jac = paddle.concat(
[self._cached_evaluate(i) for i in lazy_indexes],
axis=self._lazy_axis,
).reshape(shape)
result = part_jac[self._shifted_indexes(indexes, len(lazy_indexes))]
# squeeze redundant 1 in shape
if len(result.shape) > len(self.shape):
for _ in range(len(result.shape) - len(self.shape)):
result = result.squeeze(-1)
return result
def _cached_evaluate(self, k):
if k is None:
return self._cached_evaluate(0).reshape([])
v = self._cache.get(k)
if v is None:
v = self._evaluate(k)
self._cache[k] = v
return v
def _evaluate(self, index):
"""Evaluate one slice at along lazy axis."""
raise NotImplementedError
def _evaluate_all(self):
if len(self.shape) == 0:
return self._cached_evaluate(None)
else:
return self[:]
class _JacobianNoBatch(_Jacobian):
"""Compute Jacobian matrix without batch dimension.
Suppose the mapping is :math:`f: R^M \to R^N`, the output shape is
``(N, M)`` .
"""
def __init__(self, ys, xs):
self.is_batched = False
super().__init__(ys, xs)
# inner_shape is for convenient, it will regard 0-D tensor as 1-D tensor
self.inner_shape = [
*(self._flatten_ys.shape[0:1]),
*(self._flatten_xs.shape[0:1]),
]
self.shape = [
*(self.original_ys_shape[0:1]),
*(self.original_xs_shape[0:1]),
]
@property
def _lazy_axis(self):
return 0
def _flatten(self, xs):
if not isinstance(xs, Sequence):
return xs.reshape((-1,))
return paddle.concat(tuple(x.reshape((-1,)) for x in xs))
def _evaluate(self, row_index):
return self._flatten(
_grad_for_jacobian(
self._flatten_ys[row_index],
self._xs,
)
)
class _JacobianBatchFirst(_Jacobian):
"""Compute Jacobian matrix with batch at first axis.
Suppose the mapping is :math:`f: R^{B,M} \to R^{B,N}`, the output shape is
``(B, N, M)`` .
"""
def __init__(self, ys, xs):
self.is_batched = True
super().__init__(ys, xs)
# inner_shape is for convenient, it will regard 0-D tensor as 1-D tensor
self.inner_shape = [
*(self._flatten_xs.shape[0:1]),
*(self._flatten_ys.shape[1:2]),
*(self._flatten_xs.shape[1:2]),
]
self.shape = [
*(self._flatten_xs.shape[0:1]),
*(self.original_ys_shape[1:2]),
*(self.original_xs_shape[1:2]),
]
@property
def _lazy_axis(self):
return 1
def _flatten(self, xs):
if not isinstance(xs, Sequence):
return xs.reshape((xs.shape[0], -1))
return paddle.concat(
tuple(x.reshape((x.shape[0], -1)) for x in as_tensors(xs)), 1
)
def _evaluate(self, row_index):
return self._flatten(
_grad_for_jacobian(self._flatten_ys[:, row_index], self._xs)
)
def _multi_index(indexes, shape):
"""A tool for parsing N-dimensional index into a standard format.
Currently supporting following input format:
* ([positive|negative|slice], ...), the right-most elements can be
omitted.
The standard format after converted is slice tuple which contains N elements:
* ([positive|slice], ..., [positive|slice])
Notes:
Ellipsis indexes such as ``(..., i), (i, ...)`` is not supported.
Args:
indexes (tuple): The input indexes.
shape (tuple): The input shape.
Returns:
tuple: The standard format index as the above description.
"""
indexes = indexes if isinstance(indexes, Sequence) else (indexes,)
if any(isinstance(i, type(Ellipsis)) for i in indexes):
raise IndexError('Ellipsis index currently is not supported.')
# Fill the right-most elements.
indexes = indexes + (slice(0, None, None),) * (len(shape) - len(indexes))
# Convert to positive index.
positive_indexes = []
for i, index in enumerate(indexes):
if isinstance(index, slice):
index = slice(
index.start or 0, index.stop or shape[i], index.step or 1
)
positive_indexes.append(
slice(
index.start + shape[i] if index.start < 0 else index.start,
index.stop + shape[i] if index.stop < 0 else index.stop,
# Negative step means index backward, no need to convert to
# positive integer.
index.step,
)
)
elif isinstance(index, int):
positive_indexes.append(index + shape[i] if index < 0 else index)
else:
raise TypeError(f'Not supported index type {index}.')
return tuple(positive_indexes)
@overload
def jacobian(
ys: Tensor,
xs: Tensor,
batch_axis: int | None = ...,
) -> Jacobian: ...
@overload
def jacobian(
ys: Sequence[Tensor],
xs: Sequence[Tensor],
batch_axis: int | None = ...,
) -> tuple[tuple[Jacobian, ...], ...]: ...
@overload
def jacobian(
ys: Tensor,
xs: Sequence[Tensor],
batch_axis: int | None = ...,
) -> tuple[Jacobian, ...]: ...
@overload
def jacobian(
ys: Sequence[Tensor],
xs: Tensor,
batch_axis: int | None = ...,
) -> tuple[Jacobian, ...]: ...
def jacobian(
ys,
xs,
batch_axis=None,
):
r"""
Computes the Jacobian of the dependent variable ``ys`` versus the independent
variable ``xs``.
Where ``ys`` represents the output of ``xs`` after a certain operation, ``ys`` and
``xs`` can be Tensor or tuple of Tensors, ``batch_axis`` indicates the position of
the batch dimension of the parameter data.
When the input is a tuple Tensors, the returned result is a ``Jacobian`` object with
the same number of nesting levels as ``xs``, and each Jacobian has the same shape as
The ``xs`` tuples are identical in one-to-one correspondence.
- When ``batch_axis=None``, only 0-dimensional Tensor or 1-dimensional Tensor is
supported, assuming the shape of ``xs`` is ``[N, ]``, the shape of ``ys`` is
``[M, ]``, then the output Jacobian matrix shape is ``[M, N]``.
- When ``batch_axis=0``, only 1-dimensional Tensor or 2-dimensional Tensor is
supported, assuming the shape of ``xs`` is ``[B, N]``, The shape of ``ys`` is
``[B, M]``, then the output Jacobian matrix shape is ``[B, M, N]``.
After the ``Jacobian`` object is created, the actual calculation process does not
occur, but the lazy evaluation method is used for calculation. It can be
multi-dimensional indexed to obtain the entire Jacobian matrix or sub-matrix, and
the actual calculation will be performed at this time the value is calculated and
the result is returned. At the same time, in the actual evaluation process, the
calculated sub-matrix will be cached to avoid duplicate calculations in the
subsequent indexing process.
For example, assuming ``Jacobian`` instance ``J`` has shape ``[B, M, N]``, assuming
``M > 4`` , then ``J[:, 1:4:1, :]`` means to get the values from row ``1`` to row
``3`` of ``J``. In actual calculation, only the rows ``1`` to ``3`` are evaluated,
and the calculation results of ``1`` to ``3`` will be cached at the granularity of
the row, and will be used next time. When obtaining one or more rows of results
above, the already calculated parts will not be recalculated.
Args:
ys (Union[paddle.Tensor, Tuple[paddle.Tensor, ...]]): Output or tuple of outputs derived from xs.
xs (Union[paddle.Tensor, Tuple[paddle.Tensor, ...]]): Input or tuple of inputs.
batch_axis (Optional[int], optional): Index of batch axis. Defaults to None.
Returns:
Union[Tuple[Tuple[Jacobian, ...], ...], Tuple[Jacobian, ...], Jacobian]: Jacobian(s) of ys derived from xs.
Examples:
.. code-block:: pycon
>>> import paddle
>>> x1 = paddle.randn([3])
>>> x2 = paddle.randn([3])
>>> x1.stop_gradient = False
>>> x2.stop_gradient = False
>>> y = x1 + x2
>>> J = paddle.autograd.jacobian(y, (x1, x2))
>>> J_y_x1 = J[0][:] # evaluate result of dy/dx1
>>> J_y_x2 = J[1][:] # evaluate result of dy/dx2
>>> print(J_y_x1.shape)
paddle.Size([3, 3])
>>> print(J_y_x2.shape)
paddle.Size([3, 3])
"""
if batch_axis is not None and batch_axis != 0:
raise ValueError(
f"batch_axis should be None or 0, but got {batch_axis}."
)
# TODO(HydrogenSulfate): support batch_axis > 0
is_batched = batch_axis is not None
if isinstance(ys, Sequence) and isinstance(xs, Sequence):
_jacobian = tuple(
tuple(Jacobian(_ys, _xs, is_batched) for _xs in xs) for _ys in ys
)
elif isinstance(ys, Sequence) and not isinstance(xs, Sequence):
_jacobian = tuple(Jacobian(_ys, xs, is_batched) for _ys in ys)
elif not isinstance(ys, Sequence) and isinstance(xs, Sequence):
_jacobian = tuple(Jacobian(ys, _xs, is_batched) for _xs in xs)
else:
_jacobian = Jacobian(ys, xs, is_batched)
return _jacobian
@overload
def hessian(
ys: Tensor,
xs: Tensor,
batch_axis: int | None = ...,
) -> Hessian: ...
@overload
def hessian(
ys: Tensor,
xs: Sequence[Tensor],
batch_axis: int | None = ...,
) -> tuple[tuple[Hessian, ...], ...]: ...
def hessian(
ys,
xs,
batch_axis=None,
):
r"""
Computes the Jacobian of the dependent variable ``ys`` versus the independent
variable ``xs``.
Among them, ``ys`` means the output of ``xs`` after a certain operation, ``ys`` can
only be a single Tensor, ``xs`` can be a Tensor or a Tensor tuple, and
``batch_axis`` means The position of the batch dimension of the parameter data.
When the input ``xs`` is a Tensor tuple, the returned result is a ``Hessian`` tuple,
assuming that the internal shape of the ``xs`` tuple is composed of ``([M1, ], [M2, ])``, the shape of the returned
result consists of ``(([M1, M1], [M1, M2]), ([M2, M1], [M2, M2]))``
- When ``batch_axis=None``, only 0-dimensional Tensor or 1-dimensional Tensor is
supported, assuming that the shape of ``xs`` is ``[N, ]``, and the shape of ``ys`` is ``[ ]`` (0-dimensional Tensor), the final output is a single Hessian matrix whose shape is ``[N, N]``.
- When ``batch_axis=0``, only 1-dimensional Tensor or 2-dimensional Tensor is
supported, assuming that the shape of ``xs`` is ``[B, N]``, and the shape of ``ys`` is ``[B, ]``, the final output Jacobian matrix shape is ``[B, N, N]``.
After the ``Hessian`` object is created, the complete calculation process does not
occur, but a partial lazy evaluation method is used for calculation. It can be
multi-dimensionally indexed to obtain the entire Hessian matrix or sub-matrix. At
this time, the actual Evaluates the computation and returns the result. At the same
time, in the actual evaluation process, the calculated sub-matrix will be cached to
avoid repeated calculations in the subsequent indexing process.
Args:
ys (paddle.Tensor): Output derived from xs which contain one element.
xs (Union[paddle.Tensor, Tuple[paddle.Tensor, ...]]): Input or tuple of inputs.
batch_axis (Optional[int], optional): Index of batch axis. Defaults to None.
Returns:
Union[Tuple[Tuple[Hessian, ...], ...], Tuple[Hessian, ...], Hessian]: Hessian(s) of ys derived from xs.
Examples:
.. code-block:: pycon
>>> import paddle
>>> x1 = paddle.randn([3])
>>> x2 = paddle.randn([4])
>>> x1.stop_gradient = False
>>> x2.stop_gradient = False
>>> y = x1.sum() + x2.sum()
>>> H = paddle.autograd.hessian(y, (x1, x2))
>>> H_y_x1_x1 = H[0][0][:] # evaluate result of ddy/dx1x1
>>> H_y_x1_x2 = H[0][1][:] # evaluate result of ddy/dx1x2
>>> H_y_x2_x1 = H[1][0][:] # evaluate result of ddy/dx2x1
>>> H_y_x2_x2 = H[1][1][:] # evaluate result of ddy/dx2x2
>>> print(H_y_x1_x1.shape)
paddle.Size([3, 3])
>>> print(H_y_x1_x2.shape)
paddle.Size([3, 4])
>>> print(H_y_x2_x1.shape)
paddle.Size([4, 3])
>>> print(H_y_x2_x2.shape)
paddle.Size([4, 4])
"""
if batch_axis is None:
if ys.numel() > 1:
raise ValueError(
f"Only support ys.numel()({ys.numel()})==1 when batch_axis is None."
)
ys = ys.reshape(())
elif isinstance(batch_axis, int):
if ys[0].numel() > 1:
raise ValueError(
f"Only support ys[0].numel()({ys.numel()})==1 when batch_axis is int"
)
# TODO(HydrogenSulfate): support batch_axis > 0
if batch_axis != 0:
raise ValueError("Only support batch_axis=0 yet.")
ys = ys.reshape((-1,))
else:
raise ValueError(
f"batch_axis should be None or int, but got {type(batch_axis)}."
)
_jacobian = jacobian(ys, xs, batch_axis)
if not isinstance(xs, Sequence):
hessian = jacobian(_jacobian, xs, batch_axis)
# change classname to Hessian instead of Jacobian.
hessian.__class__ = Hessian
else:
hessian = tuple(jacobian(_j, xs, batch_axis) for _j in _jacobian)
# change classname to Hessian instead of Jacobian.
for i in range(len(hessian)):
for j in range(len(hessian[0])):
hessian[i][j].__class__ = Hessian
return hessian
def _replace_none_with_zero_tensor(xs, refs):
if xs is None:
xs = paddle.zeros_like(refs)
xs.stop_gradient = refs.stop_gradient
return xs
elif isinstance(xs, Sequence):
return tuple(
_replace_none_with_zero_tensor(x, refs[i]) for i, x in enumerate(xs)
)
else:
return xs
def _grad_for_jacobian(ys, xs, v=None):
"""A gradient function that can be used in dynamic graph and static graph.
The ``grad`` combines ``paddle.grad`` used in dynamic graph and
``paddle.static.gradients`` used in static graph, and do following changes:
* The ``allow_unused`` flag is removed and set defaults to true internally,
none in outputs will be replaced by zero tensor.
* The ``create_graph`` flag is removed and set defaults to true internally,
only makes sense in dynamic graph.
* When xs is a single Tensor, ``paddle.grad`` returns a list which only
contains one Tensor. It may confuse users, thus in this case we improve
to return a single Tensor in _grad_for_jacobian interface.
Args:
ys (Tensor|Sequence[Tensor]): The output tensor or tensor sequence of
the graph to compute gradients.
xs (Tensor|Sequence[Tensor]): The input tensor or tensor sequence of the graph to
compute gradients. The returned values of this API are the
gradients of inputs .
v (Tensor|Sequence[Tensor]|None,optional): The initial gradient values
of outputs . If grad_outputs is None, the initial gradient values of
outputs would be Tensors filled with 1; if grad_outputs is not None,
it must have the same length as outputs , and in this case, the
initial gradient value of the i-th outputs would be: (1) a Tensor
filled with 1 when the i-th element of grad_outputs is None;
(2) the i-th element of grad_outputs when the i-th element of
grad_outputs is a Tensor. Default None.
Returns:
Tensor|tuple[Tensor]: Tensor or a tuple of Tensors, whose length is the
same as the Tensor number inside inputs, and the i-th returned
Tensor is the sum of gradients of outputs with respect to the i-th
inputs.
"""
if paddle.in_dynamic_mode():
# paddle.grad returns a list though the inputs is a single Tensor. The
# follow code snippet fixes the problem by return the first element of
# xs_grad when the xs is a single Tensor.
xs_grad = paddle.grad(ys, xs, v, create_graph=True, allow_unused=True)
if (
isinstance(xs, paddle.base.framework.Variable)
and isinstance(xs_grad, Sequence)
and len(xs_grad) > 0
):
xs_grad = xs_grad[0]
else:
xs_grad = paddle.static.gradients(ys, xs, v)
if (
isinstance(xs, framework.Variable)
and isinstance(xs_grad, Sequence)
and len(xs_grad) > 0
):
xs_grad = xs_grad[0]
return _replace_none_with_zero_tensor(xs_grad, xs)
+152
View File
@@ -0,0 +1,152 @@
# Copyright (c) 2021 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.
from __future__ import annotations
from typing import TYPE_CHECKING
import paddle
from paddle.base import core, framework
from paddle.base.backward import gradients_with_optimizer # noqa: F401
from paddle.utils.download import check_and_create_dir
if TYPE_CHECKING:
from collections.abc import Sequence
from paddle import Tensor
__all__ = []
@framework.dygraph_only
def backward(
tensors: Tensor | Sequence[Tensor],
grad_tensors: Tensor | Sequence[Tensor | None] | None = None,
retain_graph: bool = False,
create_graph: bool = False,
*,
dump_backward_graph_path: str | None = None,
) -> None:
"""
Compute the backward gradients of given tensors.
Args:
tensors(list of Tensors): the tensors which the gradient to be computed. The tensors can not contain the same tensor.
grad_tensors(list of Tensors of None, optional): the init gradients of the `tensors`` .If not None, it must have the same length with ``tensors`` ,
and if any of the elements is None, then the init gradient is the default value which is filled with 1.0.
If None, all the gradients of the ``tensors`` is the default value which is filled with 1.0.
Defaults to None.
retain_graph(bool, optional): If False, the graph used to compute grads will be freed. If you would
like to add more ops to the built graph after calling this method( :code:`backward` ), set the parameter
:code:`retain_graph` to True, then the grads will be retained. Thus, setting it to False is much more memory-efficient.
Defaults to False.
dump_backward_graph_path(str, optional): Specifies the directory path for storing the debug file.
If this parameter is specified, the backward-related graph (in dot format)
and the debugging call stack information will be generated in this directory.
Returns:
NoneType: None
Examples:
.. code-block:: pycon
>>> import paddle
>>> x = paddle.to_tensor([[1, 2], [3, 4]], dtype='float32', stop_gradient=False)
>>> y = paddle.to_tensor([[3, 2], [3, 4]], dtype='float32')
>>> grad_tensor1 = paddle.to_tensor([[1, 2], [2, 3]], dtype='float32')
>>> grad_tensor2 = paddle.to_tensor([[1, 1], [1, 1]], dtype='float32')
>>> z1 = paddle.matmul(x, y)
>>> z2 = paddle.matmul(x, y)
>>> paddle.autograd.backward([z1, z2], [grad_tensor1, grad_tensor2], True)
>>> print(x.grad)
Tensor(shape=[2, 2], dtype=float32, place=Place(cpu), stop_gradient=False,
[[12., 18.],
[17., 25.]])
>>> x.clear_grad()
>>> paddle.autograd.backward([z1, z2], [grad_tensor1, None], True)
>>> print(x.grad)
Tensor(shape=[2, 2], dtype=float32, place=Place(cpu), stop_gradient=False,
[[12., 18.],
[17., 25.]])
>>> x.clear_grad()
>>> paddle.autograd.backward([z1, z2])
>>> print(x.grad)
Tensor(shape=[2, 2], dtype=float32, place=Place(cpu), stop_gradient=False,
[[10., 14.],
[10., 14.]])
"""
def check_tensors(
in_out_list: Sequence[Tensor] | Tensor, name: str
) -> Sequence[Tensor]:
assert in_out_list is not None, f"{name} should not be None"
if isinstance(in_out_list, (list, tuple)):
assert len(in_out_list) > 0, f"{name} cannot be empty"
for each_var in in_out_list:
assert isinstance(each_var, paddle.Tensor), (
f"Elements of {name} must be paddle.Tensor"
)
return in_out_list
else:
assert isinstance(in_out_list, paddle.Tensor), (
f"{name} must be Tensor or list of Tensor"
)
return [in_out_list]
tensors = check_tensors(tensors, "tensors")
assert len(tensors) == len(set(tensors)), (
"The argument 'tensors' of paddle.autograd.backward contains duplicate paddle.Tensor object."
)
if grad_tensors is not None:
if not isinstance(grad_tensors, (list, tuple)):
grad_tensors = [grad_tensors]
for each_tensor in grad_tensors:
if each_tensor is not None:
assert isinstance(each_tensor, paddle.Tensor), (
"The argument 'grad_tensors' of paddle.autograd.backward is invalid, it can be 'None', 'paddle.Tensor' or 'list[None/paddle.Tensor]'."
)
else:
grad_tensors = []
if len(grad_tensors) > 0:
assert len(tensors) == len(grad_tensors), (
"The length of grad_tensors must be equal to tensors"
)
assert isinstance(retain_graph, bool), "retain_graph must be True or False"
check_and_create_dir(dump_backward_graph_path)
core.eager.run_backward(
tensors,
grad_tensors,
retain_graph,
create_graph,
dump_backward_graph_path,
)
+810
View File
@@ -0,0 +1,810 @@
# Copyright (c) 2023 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,tes
# 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.
from __future__ import annotations
import collections
import logging
import warnings
from collections.abc import Sequence
from functools import lru_cache
from typing import Any
from paddle import pir
from paddle.base import core
from paddle.base.libpaddle.pir import (
get_used_external_value,
)
from paddle.base.wrapped_decorator import signature_safe_contextmanager
# TODO(CZ): to be removed when we support dynamic shape by default.
ALLOW_DYNAMIC_SHAPE_VJP_OPS = [
"pd_op.abs",
"pd_op.add",
"pd_op.amax",
"pd_op.amin",
"pd_op.angle",
"pd_op.argsort",
"pd_op.assign",
"pd_op.batch_norm_",
"pd_op.cast",
"pd_op.ceil",
"pd_op.concat",
"pd_op.cos",
"pd_op.cumprod",
"pd_op.cumsum",
"pd_op.divide",
"pd_op.dot",
"pd_op.dropout",
"pd_op.elementwise_pow",
"pd_op.erf",
"pd_op.exp",
"pd_op.expand",
"pd_op.floor",
"pd_op.fmax",
"pd_op.fmin",
"pd_op.gather",
"pd_op.gather_nd",
"pd_op.gelu",
"pd_op.group_norm",
"pd_op.hardsigmoid",
"pd_op.hardswish",
"pd_op.kron",
"pd_op.kthvalue",
"pd_op.layer_norm",
"pd_op.leaky_relu",
"pd_op.log",
"pd_op.logcumsumexp",
"pd_op.logsumexp",
"pd_op.linear_v2",
"pd_op.matmul",
"pd_op.max",
"pd_op.maximum",
"pd_op.mean",
"pd_op.minimum",
"pd_op.multiply",
"pd_op.pad",
"pd_op.pow",
"pd_op.prod",
"pd_op.reduce_as",
"pd_op.relu",
"pd_op.relu6",
"pd_op.reshape",
"pd_op.roll",
"pd_op.rsqrt",
"pd_op.scale",
"pd_op.scatter",
"pd_op.scatter_nd_add",
"pd_op.sigmoid",
"pd_op.silu",
"pd_op.sin",
"pd_op.softmax",
"pd_op.softsign",
"pd_op.split",
"pd_op.sqrt",
"pd_op.square",
"pd_op.squeeze",
"pd_op.stack",
"pd_op.subtract",
"pd_op.sum",
"pd_op.swiglu",
"pd_op.swish",
"pd_op.take_along_axis",
"pd_op.tanh",
"pd_op.tile",
"pd_op.topk",
"pd_op.transpose",
"pd_op.trunc",
"pd_op.unsqueeze",
"pd_op.where",
"pd_op.p_norm",
"pd_op.index_put",
"pd_op.index_add",
"pd_op.elu",
"pd_op.masked_fill",
"pd_op.masked_select",
"pd_op.var",
]
class ValueWrapper:
def __init__(self, value) -> None:
if isinstance(value, ValueWrapper):
assert isinstance(value._value, (type(None), pir.Value))
else:
if not isinstance(value, (type(None), pir.Value)):
raise TypeError(
"Value Wrapper is only support None and pir.Value"
)
self._value = value._value if isinstance(value, ValueWrapper) else value
def __hash__(self) -> int:
if isinstance(self._value, pir.Value):
return self._value.hash()
else:
return hash(self._value)
def __eq__(self, other) -> bool:
if not isinstance(other, ValueWrapper):
warnings.warn(
f'In ValueWrapper.__eq__ expected type of `other` is ValueWrapper but received {other.__class__}.'
)
return False
if self._value is None or other._value is None:
return self._value is None and other._value is None
return self._value.is_same(other._value)
class ValueDict:
def __init__(
self,
iter=None,
*,
default_factory=None,
):
self._items: dict[ValueWrapper] = {}
self._default_factory = default_factory
if iter is not None:
for key, val in iter.items():
self[key] = val
def copy(self):
ret = ValueDict()
ret._items = self._items.copy()
ret._default_factory = self._default_factory
return ret
def update(self, other_dict):
for key, val in other_dict.items():
self[key] = val
def keys(self):
for key in self._items.keys():
yield key._value
def values(self):
return self._items.values()
def items(self):
for key, val in self._items.items():
yield key._value, val
def get(self, key, default=None):
if not self.__contains__(key):
return default
return self._items[ValueWrapper(key)]
def pop(self, key):
if not self.__contains__(key):
raise KeyError(f'{key} is not in ValueDict')
return self._items.pop(ValueWrapper(key))
def setdefault(self, key, default=None):
if not self.__contains__(key):
self[key] = default
return self[key]
def __setitem__(self, key, val: Any):
self._items[ValueWrapper(key)] = val
def __getitem__(self, key):
if not self.__contains__(key):
if self._default_factory is not None:
self[key] = self._default_factory()
else:
raise KeyError(f'{key} is not in ValueDict')
return self._items[ValueWrapper(key)]
def __bool__(self):
return bool(self._items)
def __len__(self):
return len(self._items)
def __iter__(self):
return self.keys()
def __contains__(self, key):
return ValueWrapper(key) in self._items
def __repr__(self) -> str:
items_str = ", ".join(f"{key}: {val}" for key, val in self.items())
return f'ValueDict({items_str})'
class ValueSet:
def __init__(
self, iter: Sequence[ValueWrapper] | set[ValueWrapper] | None = None
):
self._set: set[ValueWrapper] = set()
if iter is not None:
for val in iter:
self.add(val)
def copy(self):
ret = ValueSet()
ret._set = self._set.copy()
return ret
def add(self, val):
if not self.__contains__(val):
self._set.add(ValueWrapper(val))
def update(self, other: set):
for val in other:
self.add(val)
def pop(self):
return self._set.pop()._value
def remove(self, val):
self._set.remove(ValueWrapper(val))
def discard(self, val):
self._set.discard(ValueWrapper(val))
def __and__(self, other: ValueSet):
return ValueSet(self._set & other._set)
def __sub__(self, other: ValueSet):
return ValueSet(self._set - other._set)
def __or__(self, other: ValueSet):
return ValueSet(self._set | other._set)
def __bool__(self):
return bool(self._set)
def __len__(self):
return len(self._set)
def __iter__(self):
for val in self._set:
yield val._value
def __contains__(self, val):
return ValueWrapper(val) in self._set
def __repr__(self) -> str:
items_str = ", ".join(repr(item) for item in self)
return f'ValueSet({items_str})'
class State:
"""
record relationship of forward op/value and backward op/value
one state must be binding with a block, if block has parent block,
state will include parent block info.
"""
def __init__(self, block):
self.block = block
# value -> list(list(value))
self.value_to_valuegrad = ValueDict(default_factory=list)
self.value_to_sumvaluegrad = ValueDict(default_factory=list)
# operation -> list(operation)
self.op_to_opgrad = collections.defaultdict(list)
# value -> list(value)
self.valuegrad_to_value = ValueDict(default_factory=list)
self.sumvaluegrad_to_value = ValueDict(default_factory=list)
# operation -> list(operation)
self.opgrad_to_op = collections.defaultdict(list)
# only for controlflow
# inside_value is sub block value, which will yield to parent block,
# parent block value is outside_value
self.inside_value_to_outside_value_map = ValueDict()
def turn_map(self) -> None:
self.valuegrad_to_value = ValueDict(default_factory=list)
self.sumvaluegrad_to_value = ValueDict(default_factory=list)
self.opgrad_to_op = collections.defaultdict(list)
for k, v in self.value_to_valuegrad.items():
if v != []:
for value in v[0]:
self.valuegrad_to_value[value] = [k]
for k, v in self.value_to_sumvaluegrad.items():
if v != []:
for value in v[0]:
self.sumvaluegrad_to_value[value] = [k]
for k, v in self.op_to_opgrad.items():
if v != []:
self.opgrad_to_op[v[0]] = [k]
def copy(self, new_block):
state = State(new_block)
state.value_to_valuegrad = self.value_to_valuegrad.copy()
state.value_to_sumvaluegrad = self.value_to_sumvaluegrad.copy()
# operation -> list(operation)
state.op_to_opgrad = self.op_to_opgrad.copy()
# value -> list(value)
state.valuegrad_to_value = self.valuegrad_to_value.copy()
state.sumvaluegrad_to_value = self.sumvaluegrad_to_value.copy()
# operation -> list(operation)
state.opgrad_to_op = self.opgrad_to_op.copy()
# only for controlflow
state.inside_value_to_outside_value_map = (
self.inside_value_to_outside_value_map.copy()
)
return state
def _check_vjp_dynamic_shape(op, inputs):
for items in inputs:
for item in items:
if (
item.is_dense_tensor_type()
and item.initialized()
and -1 in item.shape
):
return True
# Prim currently does not support dynamic shape, when dynamic shape exits in shape of op inputs, prim will be skipped its vjp op.
@signature_safe_contextmanager
def dynamic_shape_prim_vjp_guard(op, inputs):
origin_prim = core._is_bwd_prim_enabled()
if op.name() == "cf.tuple_push":
skip_prim = True
else:
skip_prim = (
origin_prim
and core._enable_prim_skip_dynamic_shape()
and _check_vjp_dynamic_shape(op, inputs)
and op.name() not in ALLOW_DYNAMIC_SHAPE_VJP_OPS
)
try:
if origin_prim and skip_prim:
core._set_prim_backward_enabled(False)
yield
finally:
if origin_prim:
core._set_prim_backward_enabled(True)
def check_type(input, input_name, expected_type, op_name, extra_message=''):
if not isinstance(input, expected_type):
raise TypeError(
f"The type of '{input_name}' in {op_name} must be {expected_type}, but received {type(input)}. {extra_message}"
)
def _as_list(x):
if x is None:
return []
return list(x) if isinstance(x, Sequence) else [x]
def some_in_set(value_list, value_set):
return any(v in value_set for v in value_list)
def is_control_flow(op):
return op.name() == "pd_op.if" or op.name() == "pd_op.while"
def is_builtin_op(op):
dialect_name, opname = op.name().split(".")
return dialect_name == "builtin"
def update_no_grad_set_by_stopgradient(block, no_grad_set):
for op in block.ops:
if is_control_flow(op):
for sub_block in op.blocks():
update_no_grad_set_by_stopgradient(sub_block, no_grad_set)
for value in op.results():
if value.stop_gradient and value not in no_grad_set:
no_grad_set.add(value)
def get_real_op_inputs(op):
if op.name() == "pd_op.if":
return get_used_external_value(op)
elif op.name() == "pd_op.while":
return op.operands_source() + get_used_external_value(
op.as_while_op().body()
)
elif op.name() == "pd_op.pylayer":
return get_used_external_value(op)
else:
return op.operands_source()
def get_real_op_outputs(op):
outputs = op.results()
if op.name() == "pd_op.array_write_":
for x in op.operands():
outputs.append(x.source())
if op.name() == "pd_op.while":
for internal_op in op.as_while_op().body().ops:
if internal_op.name() == "pd_op.array_write_":
for x in internal_op.operands():
outputs.append(x.source())
return outputs
def inverse_sort_op(old_ops):
'''
if topo graph is op1 -> op2 -> op3
return [op3, op2, op1]
'''
# init pending_count[op] which describes number of
# pending edges for its grad_op
pending_count = collections.defaultdict(int)
ops = []
[ops.append(x) for x in old_ops if x not in ops]
ops_set = set(ops)
sorted_list = []
for op in ops:
for x in get_real_op_inputs(op):
if not pir.is_fake_value(x) and x.get_defining_op() in ops_set:
pending_count[x.get_defining_op()] += 1
queue = collections.deque()
for op in ops:
if pending_count[op] == 0:
queue.append(op)
while queue:
op = queue.popleft()
sorted_list.append(op)
for x in get_real_op_inputs(op):
x_op = x.get_defining_op()
pending_count[x_op] -= 1
if pending_count[x_op] == 0:
queue.append(x_op)
if len(sorted_list) != len(ops):
raise ValueError(
"inverse_sort_op wrong, sorted_list size is not equal to origin_list size"
)
change_list = []
# true %0 = op1, 1% = increment(0%), 3% = op2(0%), tuple_push(%0, 1%, 3%),
# no one use 1% so increment be the first op, actually op2 use 1% ,
# sorted_list = [increment, op2, op1] should be [op2, increment, op1],
# tuple_push(0%) must be forward last op, backward first op, so skip it.
for op in reversed(sorted_list):
if op.name() == 'pd_op.increment_':
idx_1 = sorted_list.index(op)
idx_2 = sorted_list.index(op)
for op_in in reversed(sorted_list[: sorted_list.index(op)]):
if (
some_in_set(
op.operands_source(),
ValueSet(get_real_op_inputs(op_in)),
)
and op_in.name() != "cf.tuple_push"
):
idx_2 = sorted_list.index(op_in)
if idx_1 != idx_2:
change_list.append((idx_1, idx_2))
for idx_1, idx_2 in change_list:
sorted_list[idx_1], sorted_list[idx_2] = (
sorted_list[idx_2],
sorted_list[idx_1],
)
return sorted_list
def is_inplace_net(op_list):
'''
when program has inplace op , it's difficult to find the actual pending_count.
'''
for op in op_list:
if op.name() in ["pd_op.array_write_", "pd_op.assign_out_"]:
return True
if is_control_flow(op):
for block in op.blocks():
if is_inplace_net(block.ops):
return True
return False
def remove_op(block, op, state):
'''
remove op from block
'''
if state.opgrad_to_op[op] != []:
fwd_op = state.opgrad_to_op[op][0]
state.op_to_opgrad[fwd_op].remove(op)
for valuegrad in op.results():
if state.valuegrad_to_value[valuegrad] != []:
value = state.valuegrad_to_value[valuegrad][0]
state.value_to_valuegrad[value] = []
if value in state.sumvaluegrad_to_value:
raise ValueError(
f'input_grad in [%s] is value which need to sum {op.name()}'
)
# NOTE(SigureMo): Ensure access to the op's results before removing it.
# Otherwise, the op will be deconstructed and access the num_results
# will be undefined behavior, it always cause hanging on the macOS.
block.remove_op(op)
def while_prune_check(while_tuple_ops):
if len(while_tuple_ops) != 0:
for opresult in while_tuple_ops[0].results():
if not opresult.use_empty():
return False
return True
return False
def remove_useless_full_like_ops(block, ops, state):
'''
remove ops which are not in use recursively,
'''
remove_ops = []
inverse_ops = inverse_sort_op(list(ops))
# from output to input
for op in inverse_ops:
if op.name() == "pd_op.full_like":
if op.result(0).use_empty():
full_op = op.operand_source(1).get_defining_op()
remove_ops.append(op)
remove_ops.append(full_op)
elif is_control_flow(op):
for sub_block in op.blocks():
remove_useless_full_like_ops(sub_block, sub_block.ops, state)
for op in remove_ops:
remove_op(block, op, state)
def all_stop_gradient_true(block):
for op in block.ops:
for value in op.results():
if value.stop_gradient is False:
return False
return True
def all_input_stop_gradient_true(list_of_list):
for list_ in list_of_list:
for stop_gradient in list_:
if stop_gradient is False:
return False
return True
def all_output_grad_none(list_of_list):
for list_ in list_of_list:
for value in list_:
if value is not None:
return False
return True
def op_has_vjp(op):
# NOTE(MarioLulab): In PIR mode, even though the `PyLayer` op does
# not have a vjp interface, we still need to generate the backward
# block based on its registered backward function. To achieve this,
# we add more handling logic for `PyLayer` Op in the `call_vjp` function
return core.has_vjp(op) or op.name() == "pd_op.pylayer"
def parent_total_ops(block):
'''
when block is sub_block, forward op should include its parent block ops
(sub block nest should Add on demand to avoid block copy)
'''
total_ops = []
if block.parent_block is not None:
if block.parent_block.parent_block:
total_ops += block.parent_block.parent_block.ops
total_ops += block.parent_block.ops
total_ops += block.ops
return total_ops
# only for control_flow to find corresponding value or value_list
def return_map_value(value, map):
output = value
while output in map:
output = map[output]
return output
def return_map_value_list(value, map):
output = []
for i in range(len(value)):
if value[i] in map:
output.append(return_map_value(value[i], map))
else:
output.append(value[i])
return output
def argument_to_value(while_op):
'''
return while op's relationship of (block_argument to input value) and (input value to block_argument).
'''
if while_op.name() != "pd_op.while":
return ValueDict(), ValueDict()
assert len(while_op.as_while_op().block_arguments()) + 1 == len(
while_op.operands_source()
), (
"while op's block_arguments size + 1 should same to while op's operands_source size"
)
arg_to_value_map = ValueDict()
value_to_arg_map = ValueDict()
for arg, value in zip(
while_op.as_while_op().block_arguments(),
while_op.operands_source()[1:],
):
arg_to_value_map[arg] = value
value_to_arg_map[value] = arg
return arg_to_value_map, value_to_arg_map
def get_grad_semantic_info(op):
'''
return whether op's inputs has grad, usually handled from yaml.
some op has uncertain inputs need special handling.
'''
if op.name() in [
"builtin.combine",
"pd_op.if",
"pd_op.while",
"pd_op.pylayer",
"cf.tuple_push",
"dist_op.moe_global_mesh_tensor",
"dist_op.moe_sub_mesh_tensors",
"dist_op.dist_reshape",
]:
grad_semantic_info = [True for _ in range(len(get_real_op_inputs(op)))]
if op.name() == "pd_op.if":
grad_semantic_info[0] = False
else:
grad_semantic_info = op.get_input_grad_semantics()
return grad_semantic_info
def get_split_op(value):
for op in value.all_used_ops():
if op.name() == "builtin.split":
return op
return None
@lru_cache
def warning_once(message: str):
logging.warning(message)
def update_if_output_stopgradient(if_op, true_yield_op, false_yield_op):
"""
Update if_op's stop_gradient based on true_yield_op and false_yield_op.
Args:
true_yield_op: true block of if_op's last op.
false_yield_op: false block of if_op's last op.
if_op: update it's op_results()'s stop_gradient.
"""
if (
true_yield_op.name() != 'cf.yield'
or false_yield_op.name() != 'cf.yield'
):
raise ValueError("param is not yield op")
# Check if operands_source sizes match
if len(true_yield_op.operands_source()) != len(
false_yield_op.operands_source()
):
raise ValueError("Mismatched yield operands_source sizes")
# Check if op_results size matches operands_source
if len(if_op.results()) != len(true_yield_op.operands_source()):
raise ValueError(
"Mismatched if op_results size with yield operands_source"
)
# Update if_op's stop_gradient
for i in range(len(true_yield_op.operands_source())):
stop_grad1 = true_yield_op.operand_source(i).stop_gradient
stop_grad2 = false_yield_op.operand_source(i).stop_gradient
# Set to False if either stop_gradient is False
if not stop_grad1 or not stop_grad2:
if_op.result(i).stop_gradient = False
def update_while_output_stopgradient(while_op, yield_op):
"""
Update while_op's stop_gradient based on yield_op.
Args:
yield_op: The yield operation associated with the while loop.
while_op: The while operation whose op_results()'s stop_gradient needs to be updated.
"""
# Check if yield_op is indeed a yield operation
if yield_op.name() != 'cf.yield':
raise ValueError("yield_op is not a yield operation")
# Check if operands_source size of yield_op matches op_results size of while_op
if len(while_op.results()) + 1 != len(yield_op.operands_source()):
raise ValueError(
f"Mismatched while op_results size %d with yield operands_source %d. {len(while_op.results()) + 1, len(yield_op.operands_source())}"
)
# Update while_op's stop_gradient
for i in range(1, len(yield_op.operands_source())):
stop_grad = yield_op.operand_source(i).stop_gradient
# Set to False if stop_gradient is False
if not stop_grad:
while_op.result(i - 1).stop_gradient = False
def find_index_of_yield(value, yield_op):
for i, v in enumerate(yield_op.operands_source()):
if v.is_same(value):
return i
return -1
def update_tuple_pop_origin_inputs(tuple_pop_outputs):
if tuple_pop_outputs == []:
return tuple_pop_outputs
op = tuple_pop_outputs[0][0].get_defining_op()
assert op.name() == "cf.tuple_pop"
stack_op = op.operand_source(0).get_defining_op()
tuple_push_inputs = stack_op.result(1).first_use().owner().operands_source()
tuple_push_inputs_with_if = []
for input in tuple_push_inputs:
if input.first_use().owner().name() == "cf.yield":
yield_op = input.first_use().owner()
index = find_index_of_yield(input, yield_op)
assert index != -1
tuple_push_inputs_with_if.append(
yield_op.get_parent_block().parent_op.result(index)
)
else:
tuple_push_inputs_with_if.append(input)
# pass inlets
return tuple_push_inputs_with_if[1:]
def value_in_block(value, block):
value_block = value.get_defining_op().get_parent_block()
while block.parent_op.name() != "builtin.module":
if block == value_block:
return True
block = block.parent_block
# now block is module op's block
if block == value_block:
return True
return False
+18
View File
@@ -0,0 +1,18 @@
# 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.
from .py_layer import (
PyLayerContext as FunctionCtx, # noqa: F401
once_differentiable, # noqa: F401
)
+15
View File
@@ -0,0 +1,15 @@
# 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.
from paddle.base.dygraph.base import set_grad_enabled # noqa: F401
File diff suppressed because it is too large Load Diff
+504
View File
@@ -0,0 +1,504 @@
# Copyright (c) 2021 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.
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Concatenate, TypeVar
import paddle
from paddle.base import core
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from paddle import Tensor
__all__ = []
_RetT = TypeVar('_RetT')
class PyLayerContext:
"""
``PyLayerContext`` can assist the :ref:`api_paddle_autograd_PyLayer` in implementing certain functionalities.
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> class cus_tanh(PyLayer):
... @staticmethod
... def forward(ctx, x):
... # ctx is a object of PyLayerContext.
... y = paddle.tanh(x)
... ctx.save_for_backward(y)
... return y
...
... @staticmethod
... def backward(ctx, dy):
... # ctx is a object of PyLayerContext.
... (y,) = ctx.saved_tensor()
... grad = dy * (1 - paddle.square(y))
... return grad
"""
container: tuple[Tensor, ...]
not_inplace_tensors: tuple[Tensor, ...]
non_differentiable: tuple[Tensor, ...]
materialize_grads: bool
grad_in_dtype_consistent: bool
def set_grad_in_dtype_consistent(self, flag: bool) -> None:
"""
Set whether to maintain gradient input dtype consistency between forward output and backward input.
Note:
This API should be called only inside `forward`.
By default, backward input gradients are automatically cast to match the dtype of forward outputs.
Set this to `False` to disable automatic casting and maintain original gradient dtypes in backward.
Args:
flag (bool): Whether to enable automatic dtype conversion in backward.
- `True`: Cast backward input gradient to match forward output dtype (default behavior)
- `False`: Preserve original dtype of backward input gradient
Returns:
None
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> paddle.seed(2025)
>>> class cus_tanh(PyLayer):
... @staticmethod
... def forward(ctx, x):
... y = paddle.tanh(x)
... # Pass tensors to backward.
... ctx.save_for_backward(y)
... # The gradient input in the backward process
... # will not be automatically cast to the dtype of the forward output.
... ctx.set_grad_in_dtype_consistent(False)
... return y
...
... @staticmethod
... def backward(ctx, dy):
...
... # Get the tensors passed by forward.
... (y,) = ctx.saved_tensor()
... grad = dy * (1 - paddle.square(y))
... return grad
>>> class cus_tanh_cast_grad(PyLayer):
... @staticmethod
... def forward(ctx, x):
... y = paddle.tanh(x)
... # Pass tensors to backward.
... ctx.save_for_backward(y)
... return y
...
... @staticmethod
... def backward(ctx, dy):
... # Get the tensors passed by forward.
... (y,) = ctx.saved_tensor()
... grad = dy * (1 - paddle.square(y))
... # The gradient input in cus_tanh be cast to bfloat16 manually,
... # and cus_tanh will not cast the gradient to the dtype of the forward output.
... grad = paddle.cast(grad, paddle.float16)
... return grad
>>> x = paddle.randn([3, 3]).astype("float32")
>>> x.stop_gradient = False
>>> y = cus_tanh.apply(x)
>>> z = cus_tanh_cast_grad.apply(y)
>>> z.sum().backward()
"""
self.grad_in_dtype_consistent = flag
def save_for_backward(self, *tensors: Tensor) -> None:
"""
Saves given tensors that backward need. Use ``saved_tensor`` in the `backward` to get the saved tensors.
Note:
This API should be called at most once, and only inside `forward`.
Args:
tensors(list of Tensors): Tensors to be stored.
Returns:
None
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> class cus_tanh(PyLayer):
... @staticmethod
... def forward(ctx, x):
... # ctx is a context object that store some objects for backward.
... y = paddle.tanh(x)
... # Pass tensors to backward.
... ctx.save_for_backward(y)
... return y
...
... @staticmethod
... def backward(ctx, dy):
... # Get the tensors passed by forward.
... (y,) = ctx.saved_tensor()
... grad = dy * (1 - paddle.square(y))
... return grad
"""
self.container = tensors
def saved_tensor(self) -> tuple[Tensor, ...]:
"""
Get the tensors stored by ``save_for_backward``.
Returns:
list of Tensors or None: If context contains tensors stored by `save_for_backward`,
then return these tensors, otherwise return None.
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> class cus_tanh(PyLayer):
... @staticmethod
... def forward(ctx, x):
... # ctx is a context object that store some objects for backward.
... y = paddle.tanh(x)
... # Pass tensors to backward.
... ctx.save_for_backward(y)
... return y
...
... @staticmethod
... def backward(ctx, dy):
... # Get the tensors passed by forward.
... (y,) = ctx.saved_tensor()
... grad = dy * (1 - paddle.square(y))
... return grad
"""
return self.container
@property
def saved_tensors(self):
"""
Get the tensors stored by ``save_for_backward``. This attribute is an alias for the method ``saved_tensor()``.
Returns:
list of Tensors or None: If context contains tensors stored by `save_for_backward`,
then return these tensors, otherwise return None.
"""
return self.saved_tensor()
def mark_not_inplace(self, *args: Tensor) -> None:
"""
Marks inputs as not inplace.
This should be called at most once, only from inside the `forward` method,
and all arguments should be Tensor inputs.
If the Tensor returned by `forward` method is the same as the Tensor input of forward,
and this Tensor is marked as not_inplace, then Paddle will help the user create a new Tensor as output.
Thereby preventing the auto grad information of the input Tensor from being overwritten.
Examples:
.. code-block:: pycon
>>> import paddle
>>> class Exp(paddle.autograd.PyLayer):
... @staticmethod
... def forward(ctx, x):
... ctx.mark_not_inplace(x)
... return x
...
... @staticmethod
... def backward(ctx, grad_output):
... out = grad_output.exp()
... return out
>>> paddle.seed(2023)
>>> x = paddle.randn((1, 1))
>>> x.stop_gradient = False
>>> attn_layers = []
>>> for idx in range(0, 2):
... attn_layers.append(Exp())
>>> for step in range(0, 2):
... a = x
... for j in range(0, 2):
... a = attn_layers[j].apply(x)
... a.backward()
"""
self.not_inplace_tensors = args
def mark_non_differentiable(self, *args: Tensor) -> None:
"""
Marks outputs as non-differentiable.
This should be called at most once, only from inside the `forward` method,
and all arguments should be tensor outputs.
This will mark outputs as not requiring gradients, increasing the
efficiency of backward computation. You still need to accept a gradient
for each output in `backward`, but it's always going to
be a zero tensor with the same shape as the shape of a corresponding
output.
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> import numpy as np
>>> class Tanh(PyLayer):
... @staticmethod
... def forward(ctx, x):
... a = x + x
... b = x + x + x
... ctx.mark_non_differentiable(a)
... return a, b
...
... @staticmethod
... def backward(ctx, grad_a, grad_b):
... assert np.equal(grad_a.numpy(), paddle.zeros([1]).numpy())
... assert np.equal(grad_b.numpy(), paddle.ones([1], dtype="float64").numpy())
... return grad_b
>>> x = paddle.ones([1], dtype="float64")
>>> x.stop_gradient = False
>>> a, b = Tanh.apply(x)
>>> b.sum().backward()
"""
self.non_differentiable = args
def set_materialize_grads(self, value: bool) -> None:
"""
Sets whether to materialize output grad tensors. Default is True.
This should be called only from inside the `forward` method.
If True, undefined output grad tensors will be expanded to tensors full
of zeros prior to calling the `backward` method.
If False, undefined output grad tensors will be None.
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> import numpy as np
>>> class Tanh(PyLayer):
... @staticmethod
... def forward(ctx, x):
... return x + x + x, x + x
...
... @staticmethod
... def backward(ctx, grad, grad2):
... assert np.equal(grad2.numpy(), paddle.zeros([1]).numpy())
... return grad
>>> class Tanh2(PyLayer):
... @staticmethod
... def forward(ctx, x):
... ctx.set_materialize_grads(False)
... return x + x + x, x + x
...
... @staticmethod
... def backward(ctx, grad, grad2):
... assert grad2 == None
... return grad
>>> x = paddle.ones([1], dtype="float64")
>>> x.stop_gradient = False
>>> Tanh.apply(x)[0].backward()
>>> x2 = paddle.ones([1], dtype="float64")
>>> x2.stop_gradient = False
>>> Tanh2.apply(x2)[0].backward()
"""
self.materialize_grads = value
class PyLayerBackward(core.eager.PyLayer, PyLayerContext):
def backward(self, *args):
return self._forward_cls.backward(self, *args)
class PyLayerMeta(type):
def __init__(cls, name, bases, attrs):
cls._backward_function = type(
name + '_backward', (PyLayerBackward,), {"_forward_cls": cls}
)
super().__init__(name, bases, attrs)
class PyLayer(core.eager.PyLayer, PyLayerContext, metaclass=PyLayerMeta):
"""
Paddle implements Python custom operators on the PaddlePaddle framework by creating a subclass of
``PyLayer``, which must comply with the following rules:
1. The subclass must contain static ``forward`` and ``backward`` functions, with the first argument being
:ref:`api_paddle_autograd_PyLayerContext`. If a returned value in ``backward`` corresponds to a ``Tensor`` that
requires gradients in ``forward``, the returned value must be a ``Tensor``.
2. Except for the first argument, other arguments of ``backward`` are gradients of the output ``Tensors``
of ``forward``. Therefore, the number of input ``Tensor`` in ``backward`` must be the same as the number
of output ``Tensor`` in ``forward``. If you need to use input ``Tensor`` from ``forward`` in ``backward``,
you can save these ``Tensors`` by inputting them into :ref:`api_paddle_autograd_PyLayerContext`'s
``save_for_backward`` method and use them in ``backward`` later.
3. The output of ``backward`` can be ``Tensor`` or ``list/tuple(Tensor)``, which are gradients of the
output ``Tensor`` of ``forward``. Therefore, the number of output ``Tensor`` in ``backward`` is the same
as the number of input ``Tensor`` in ``forward``.
After building the custom operator, apply it by running the ``apply`` method.
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> class cus_tanh(PyLayer):
... @staticmethod
... def forward(ctx, x):
... y = paddle.tanh(x)
... # Pass tensors to backward.
... ctx.save_for_backward(y)
... return y
...
... @staticmethod
... def backward(ctx, dy):
... # Get the tensors passed by forward.
... (y,) = ctx.saved_tensor()
... grad = dy * (1 - paddle.square(y))
... return grad
>>> paddle.seed(2023)
>>> data = paddle.randn([2, 3], dtype="float64")
>>> data.stop_gradient = False
>>> z = cus_tanh.apply(data)
>>> z.mean().backward()
>>> print(data.grad)
Tensor(shape=[2, 3], dtype=float64, place=Place(cpu), stop_gradient=True,
[[0.16604150, 0.05858341, 0.14051214],
[0.15677770, 0.01564609, 0.02991660]])
"""
@staticmethod
def forward(
ctx: PyLayerContext, *args: Any, **kwargs: Any
) -> Tensor | Sequence[Tensor]:
"""
It is to be overloaded by subclasses. It must accept a object of :ref:`api_paddle_autograd_PyLayerContext` as
the first argument, followed by any number of arguments (tensors or other types).
`None` can not be included in the returned result.
Args:
*args(tuple): input of PyLayer.
**kwargs(dict): input of PyLayer.
Returns:
tensors or other types : output of PyLayer.
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> class cus_tanh(PyLayer):
... @staticmethod
... def forward(ctx, x):
... y = paddle.tanh(x)
... # Pass tensors to backward.
... ctx.save_for_backward(y)
... return y
...
... @staticmethod
... def backward(ctx, dy):
... # Get the tensors passed by forward.
... (y,) = ctx.saved_tensor()
... grad = dy * (1 - paddle.square(y))
... return grad
"""
raise NotImplementedError(
"You must implement the forward function for PyLayer."
)
@staticmethod
def backward(ctx: PyLayerContext, *args: Any) -> Tensor | Sequence[Tensor]:
"""
This is a function to calculate the gradient. It is to be overloaded by subclasses.
It must accept a object of :ref:`api_paddle_autograd_PyLayerContext` as the first
argument, and the rest arguments are the gradient of forward's output tensors.
Output tensors of backward are the gradient of forward's input tensors.
Args:
*args(tuple): The gradient of forward's output tensor(s).
**kwargs(dict): The gradient of forward's output tensor(s).
Returns:
Tensor or list of Tensors: The gradient of forward's input tensor(s).
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> class cus_tanh(PyLayer):
... @staticmethod
... def forward(ctx, x):
... y = paddle.tanh(x)
... # Pass tensors to backward.
... ctx.save_for_backward(y)
... return y
...
... @staticmethod
... def backward(ctx, dy):
... # Get the tensors passed by forward.
... (y,) = ctx.saved_tensor()
... grad = dy * (1 - paddle.square(y))
... return grad
"""
raise NotImplementedError(
"You must implement the backward function for PyLayer."
)
def once_differentiable(
backward: Callable[Concatenate[PyLayerContext, ...], _RetT],
) -> Callable[Concatenate[PyLayerContext, ...], _RetT]:
def wrapper(ctx: PyLayerContext, *args: Any) -> _RetT:
with paddle.base.dygraph.no_grad():
outputs = backward(ctx, *args)
return outputs
return wrapper
@@ -0,0 +1,127 @@
# Copyright (c) 2022 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.
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from paddle.base import core
if TYPE_CHECKING:
from collections.abc import Callable
from paddle import Tensor
__all__ = []
class saved_tensors_hooks:
"""
Dynamic graph, registers a pair of pack / unpack hooks for saved tensors.
Parameters:
pack_hook (function): The pack hook will be called every time the forward
operation inputs/outputs tensors need be saved for backward. Then you
can save it to CPU or Disk. The input of `pack_hook` is a tensor need
be saved. The output of `pack_hook` is then stored information instead
of the original tensor. `pack_hook` will also be called while any
tensor need be saved by `PyLayerContext.save_for_backward`. If a tensor
saved for backward is no need buffer, `pack_hook` will not be called.
Only the tensor saved for backward is DenseTensor, `pack_hook` will be
called.
unpack_hook (function): The unpack hook will be called every time the
backward need use the saved inputs/outputs tensors. Then you can reload
the tensor and return it to paddle framework. The input of `unpack_hook`
is the information returned by `pack_hook`. The output of `unpack_hook`
is a tensor reloaded by the information, and the tensor must has the same
content as the original tensor passed as input to the corresponding
`pack_hook`.
Returns:
None
Examples:
.. code-block:: pycon
:name: code-example1
>>> # Example1
>>> import paddle
>>> def pack_hook(x):
... print("Packing", x)
... return x.numpy()
>>> def unpack_hook(x):
... print("UnPacking", x)
... return paddle.to_tensor(x)
>>> a = paddle.ones([3, 3])
>>> b = paddle.ones([3, 3]) * 2
>>> a.stop_gradient = False
>>> b.stop_gradient = False
>>> with paddle.autograd.saved_tensors_hooks(pack_hook, unpack_hook):
... y = paddle.multiply(a, b)
>>> y.sum().backward()
.. code-block:: pycon
:name: code-example2
>>> # Example2
>>> import paddle
>>> from paddle.autograd import PyLayer
>>> class cus_multiply(PyLayer):
... @staticmethod
... def forward(ctx, a, b):
... y = paddle.multiply(a, b)
... ctx.save_for_backward(a, b)
... return y
...
... @staticmethod
... def backward(ctx, dy):
... a, b = ctx.saved_tensor()
... grad_a = dy * a
... grad_b = dy * b
... return grad_a, grad_b
>>> def pack_hook(x):
... print("Packing", x)
... return x.numpy()
>>> def unpack_hook(x):
... print("UnPacking", x)
... return paddle.to_tensor(x)
>>> a = paddle.ones([3, 3])
>>> b = paddle.ones([3, 3]) * 2
>>> a.stop_gradient = False
>>> b.stop_gradient = False
>>> with paddle.autograd.saved_tensors_hooks(pack_hook, unpack_hook):
... y = cus_multiply.apply(a, b)
>>> y.sum().backward()
"""
def __init__(
self,
pack_hook: Callable[[Tensor], Any | None],
unpack_hook: Callable[[Any], Tensor | None],
) -> None:
self.pack_hook = pack_hook
self.unpack_hook = unpack_hook
def __enter__(self) -> None:
core.eager.register_saved_tensors_hooks(
self.pack_hook, self.unpack_hook
)
def __exit__(self, *args: object) -> None:
core.eager.reset_saved_tensors_hooks()