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

455 lines
14 KiB
Python

# 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.
import enum
import sys
import typing
import numpy as np
import paddle
from paddle.incubate.autograd.utils import as_tensors
##########################################################
# Finite Difference Utils
##########################################################
def _product(t):
return int(np.prod(t))
def _get_item(t, idx):
assert isinstance(t, paddle.base.framework.Variable), (
"The first argument t must be Tensor."
)
assert isinstance(idx, int), (
"The second argument idx must be an int number."
)
flat_t = paddle.reshape(t, [-1])
return flat_t.__getitem__(idx)
def _set_item(t, idx, value):
assert isinstance(t, paddle.base.framework.Variable), (
"The first argument t must be Tensor."
)
assert isinstance(idx, int), (
"The second argument idx must be an int number."
)
flat_t = paddle.reshape(t, [-1])
flat_t.__setitem__(idx, value)
return paddle.reshape(flat_t, t.shape)
def _compute_numerical_jacobian(func, xs, delta, np_dtype):
xs = list(as_tensors(xs))
ys = list(as_tensors(func(*xs)))
fin_size = len(xs)
fout_size = len(ys)
jacobian = [[] for _ in range(fout_size)]
for i in range(fout_size):
jac_i = [[] for _ in range(fin_size)]
for j in range(fin_size):
jac_i[j] = np.zeros(
(_product(ys[i].shape), _product(xs[j].shape)), dtype=np_dtype
)
jacobian[i] = jac_i
for j in range(fin_size):
for q in range(_product(xs[j].shape)):
orig = _get_item(xs[j], q)
orig = paddle.assign(orig)
x_pos = orig + delta
xs[j] = _set_item(xs[j], q, x_pos)
ys_pos = as_tensors(func(*xs))
x_neg = orig - delta
xs[j] = _set_item(xs[j], q, x_neg)
ys_neg = as_tensors(func(*xs))
xs[j] = _set_item(xs[j], q, orig)
for i in range(fout_size):
for p in range(_product(ys[i].shape)):
y_pos = _get_item(ys_pos[i], p)
y_neg = _get_item(ys_neg[i], p)
jacobian[i][j][p][q] = (y_pos - y_neg) / delta / 2.0
return jacobian
def _compute_numerical_hessian(func, xs, delta, np_dtype):
xs = list(as_tensors(xs))
ys = list(as_tensors(func(*xs)))
fin_size = len(xs)
hessian = [[] for _ in range(fin_size)]
for i in range(fin_size):
hessian_i = [[] for _ in range(fin_size)]
for j in range(fin_size):
hessian_i[j] = np.zeros(
(_product(xs[i].shape), _product(xs[j].shape)), dtype=np_dtype
)
hessian[i] = hessian_i
for i in range(fin_size):
for p in range(_product(xs[i].shape)):
for j in range(fin_size):
for q in range(_product(xs[j].shape)):
orig = _get_item(xs[j], q)
orig = paddle.assign(orig)
x_pos = orig + delta
xs[j] = _set_item(xs[j], q, x_pos)
jacobian_pos = _compute_numerical_jacobian(
func, xs, delta, np_dtype
)
x_neg = orig - delta
xs[j] = _set_item(xs[j], q, x_neg)
jacobian_neg = _compute_numerical_jacobian(
func, xs, delta, np_dtype
)
xs[j] = _set_item(xs[j], q, orig)
hessian[i][j][p][q] = (
(jacobian_pos[0][i][0][p] - jacobian_neg[0][i][0][p])
/ delta
/ 2.0
)
return hessian
def concat_to_matrix(xs, is_batched=False):
"""Concats a tuple of tuple of Jacobian/Hessian matrix into one matrix"""
rows = []
for i in range(len(xs)):
rows.append(np.concatenate(list(xs[i]), -1))
return np.concatenate(rows, 1) if is_batched else np.concatenate(rows, 0)
def _compute_numerical_batch_jacobian(
func, xs, delta, np_dtype, merge_batch=True
):
no_batch_jacobian = _compute_numerical_jacobian(func, xs, delta, np_dtype)
xs = list(as_tensors(xs))
ys = list(as_tensors(func(*xs)))
fin_size = len(xs)
fout_size = len(ys)
bs = xs[0].shape[0]
bat_jac = []
for i in range(fout_size):
batch_jac_i = []
for j in range(fin_size):
jac = no_batch_jacobian[i][j]
jac_shape = jac.shape
out_size = jac_shape[0] // bs
in_size = jac_shape[1] // bs
jac = np.reshape(jac, (bs, out_size, bs, in_size))
batch_jac_i_j = np.zeros(shape=(out_size, bs, in_size))
for p in range(out_size):
for b in range(bs):
for q in range(in_size):
batch_jac_i_j[p][b][q] = jac[b][p][b][q]
if merge_batch:
batch_jac_i_j = np.reshape(batch_jac_i_j, (out_size, -1))
batch_jac_i.append(batch_jac_i_j)
bat_jac.append(batch_jac_i)
return bat_jac
def _compute_numerical_batch_hessian(func, xs, delta, np_dtype):
xs = list(as_tensors(xs))
batch_size = xs[0].shape[0]
fin_size = len(xs)
hessian = []
for b in range(batch_size):
x_l = []
for j in range(fin_size):
x_l.append(paddle.reshape(xs[j][b], shape=[1, -1]))
hes_b = _compute_numerical_hessian(func, x_l, delta, np_dtype)
if fin_size == 1:
hessian.append(hes_b[0][0])
else:
hessian.append(hes_b)
hessian_res = []
for index in range(fin_size):
x_reshape = paddle.reshape(xs[index], shape=[batch_size, -1])
for index_ in range(fin_size):
for i in range(x_reshape.shape[1]):
tmp = []
for j in range(batch_size):
if fin_size == 1:
tmp.extend(hessian[j][i])
else:
tmp.extend(hessian[j][i][index_][index])
hessian_res.append(tmp)
if fin_size == 1:
return hessian_res
hessian_result = []
mid = len(hessian_res) // 2
for i in range(mid):
hessian_result.append(
np.stack((hessian_res[i], hessian_res[mid + i]), axis=0)
)
return hessian_result
def _compute_numerical_vjp(func, xs, v, delta, np_dtype):
xs = as_tensors(xs)
jacobian = np.array(_compute_numerical_jacobian(func, xs, delta, np_dtype))
if v is None:
v = [paddle.ones_like(x) for x in xs]
flat_v = np.array([v_el.numpy().reshape(-1) for v_el in v])
vjp = [np.zeros((_product(x.shape)), dtype=np_dtype) for x in xs]
for j in range(len(xs)):
for q in range(_product(xs[j].shape)):
vjp[j][q] = np.sum(
jacobian[:, j, :, q].reshape(flat_v.shape) * flat_v
)
vjp = [vjp[j].reshape(xs[j].shape) for j in range(len(xs))]
return vjp
def _compute_numerical_vhp(func, xs, v, delta, np_dtype):
xs = list(as_tensors(xs))
hessian = np.array(_compute_numerical_hessian(func, xs, delta, np_dtype))
flat_v = np.array([v_el.numpy().reshape(-1) for v_el in v])
vhp = [np.zeros((_product(x.shape)), dtype=np_dtype) for x in xs]
for j in range(len(xs)):
for q in range(_product(xs[j].shape)):
vhp[j][q] = np.sum(
hessian[:, j, :, q].reshape(flat_v.shape) * flat_v
)
vhp = [vhp[j].reshape(xs[j].shape) for j in range(len(xs))]
return vhp
##########################################################
# TestCases of different function.
##########################################################
def reduce(x):
return paddle.sum(x)
def reduce_dim(x):
return paddle.sum(x, axis=0)
def matmul(x, y):
return paddle.matmul(x, y)
def mul(x, y):
return x * y
def pow(x, y):
return paddle.pow(x, y)
def o2(x, y):
return paddle.multiply(x, y), paddle.matmul(x, y.t())
def unuse(x, y):
return paddle.sum(x)
def nested(x):
def inner(y):
return x * y
return inner
def square(x):
return x * x
##########################################################
# Parameterized Test Utils.
##########################################################
TEST_CASE_NAME = 'suffix'
def place(devices, key='place'):
"""A Decorator for a class which will make the class running on different
devices .
Args:
devices (Sequence[Paddle.CUDAPlace|Paddle.CPUPlace]): Device list.
key (str, optional): Defaults to 'place'.
"""
def decorate(cls):
module = sys.modules[cls.__module__].__dict__
raw_classes = {
k: v for k, v in module.items() if k.startswith(cls.__name__)
}
for raw_name, raw_cls in raw_classes.items():
for d in devices:
test_cls = dict(raw_cls.__dict__)
test_cls.update({key: d})
new_name = raw_name + '.' + d.__class__.__name__
module[new_name] = type(new_name, (raw_cls,), test_cls)
del module[raw_name]
return cls
return decorate
def parameterize(fields, values=None):
"""Decorator for a unittest class which make the class running on different
test cases.
Args:
fields (Sequence): The field name sequence of test cases.
values (Sequence, optional): The test cases sequence. Defaults to None.
"""
fields = [fields] if isinstance(fields, str) else fields
params = [dict(zip(fields, vals)) for vals in values]
def decorate(cls):
test_cls_module = sys.modules[cls.__module__].__dict__
for i, values in enumerate(params):
test_cls = dict(cls.__dict__)
values = {
k: staticmethod(v) if callable(v) else v
for k, v in values.items()
}
test_cls.update(values)
name = cls.__name__ + str(i)
name = (
name + '.' + values.get('suffix')
if values.get('suffix')
else name
)
test_cls_module[name] = type(name, (cls,), test_cls)
for m in list(cls.__dict__):
if m.startswith("test"):
delattr(cls, m)
return cls
return decorate
##########################################################
# Utils for transpose different Jacobian/Hessian matrix format.
##########################################################
# B is batch size, N is row size, M is column size.
MatrixFormat = enum.Enum('MatrixFormat', ('NBM', 'BNM', 'NMB', 'NM'))
def _np_transpose_matrix_format(src, src_format, des_format):
"""Transpose Jacobian/Hessian matrix format."""
supported_format = (MatrixFormat.NBM, MatrixFormat.BNM, MatrixFormat.NMB)
if src_format not in supported_format or des_format not in supported_format:
raise ValueError(
f"Supported Jacobian format is {supported_format}, but got src: {src_format}, des: {des_format}"
)
src_axis = {c: i for i, c in enumerate(src_format.name)}
dst_axis = tuple(src_axis[c] for c in des_format.name)
return np.transpose(src, dst_axis)
def _np_concat_matrix_sequence(src, src_format=MatrixFormat.NM):
"""Convert a sequence of sequence of Jacobian/Hessian matrix into one huge
matrix."""
def concat_col(xs):
if src_format in (MatrixFormat.NBM, MatrixFormat.BNM, MatrixFormat.NM):
return np.concatenate(xs, axis=-1)
else:
return np.concatenate(xs, axis=1)
def concat_row(xs):
if src_format in (MatrixFormat.NBM, MatrixFormat.NM, MatrixFormat.NMB):
return np.concatenate(xs, axis=0)
else:
return np.concatenate(xs, axis=1)
supported_format = (
MatrixFormat.NBM,
MatrixFormat.BNM,
MatrixFormat.NMB,
MatrixFormat.NM,
)
if src_format not in supported_format:
raise ValueError(
f"Supported Jacobian format is {supported_format}, but got {src_format}"
)
if not isinstance(src, typing.Sequence):
return src
if not isinstance(src[0], typing.Sequence):
src = [src]
return concat_row(tuple(concat_col(xs) for xs in src))
##########################################################
# Utils for generating test data.
##########################################################
def gen_static_data_and_feed(xs, v, stop_gradient=True):
feed = {}
if isinstance(xs, typing.Sequence):
static_xs = []
for i, x in enumerate(xs):
x = paddle.static.data(f"x{i}", x.shape, x.dtype)
x.stop_gradient = stop_gradient
static_xs.append(x)
feed.update({f'x{idx}': value for idx, value in enumerate(xs)})
else:
static_xs = paddle.static.data('x', xs.shape, xs.dtype)
static_xs.stop_gradient = stop_gradient
feed.update({'x': xs})
if isinstance(v, typing.Sequence):
static_v = []
for i, e in enumerate(v):
e = paddle.static.data(f'v{i}', e.shape, e.dtype)
e.stop_gradient = stop_gradient
static_v.append(e)
feed.update({f'v{i}': value for i, value in enumerate(v)})
elif v is not None:
static_v = paddle.static.data('v', v.shape, v.dtype)
static_v.stop_gradient = stop_gradient
feed.update({'v': v})
else:
static_v = v
return feed, static_xs, static_v
def gen_static_inputs_and_feed(xs, stop_gradient=True):
feed = {}
if isinstance(xs, typing.Sequence):
static_xs = []
for i, x in enumerate(xs):
x = paddle.static.data(f"x{i}", x.shape, x.dtype)
x.stop_gradient = stop_gradient
static_xs.append(x)
feed.update({f'x{idx}': value for idx, value in enumerate(xs)})
else:
static_xs = paddle.static.data('x', xs.shape, xs.dtype)
static_xs.stop_gradient = stop_gradient
feed.update({'x': xs})
return feed, static_xs