From 0773157a2f030da2216e55d87eeb23555fc1ffd6 Mon Sep 17 00:00:00 2001 From: KateJing1212 <114161358+KateJing1212@users.noreply.github.com> Date: Fri, 24 Mar 2023 12:52:01 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90PaddlePaddle=20Hackathon=20=E7=AC=AC?= =?UTF-8?q?=E5=9B=9B=E6=9C=9F=E3=80=91No.19=EF=BC=9A=E4=B8=BA=20Paddle=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20polygamma=20API=20(#472)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create 20220320_api_design_for_polygamma.md * Create 20230320_api_design_for_polygamma.md * apidoctest * review0322 * review0323 * 'review0323(2)' * 'review0323(3)' * 'review0324' --- .../APIs/20230324_api_design_for_polygamma.md | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 rfcs/APIs/20230324_api_design_for_polygamma.md diff --git a/rfcs/APIs/20230324_api_design_for_polygamma.md b/rfcs/APIs/20230324_api_design_for_polygamma.md new file mode 100644 index 000000000..cf5b0c27e --- /dev/null +++ b/rfcs/APIs/20230324_api_design_for_polygamma.md @@ -0,0 +1,374 @@ +# paddle.polygamma 设计文档 + +|API名称 | paddle.polygamma | +|---|---| +|提交作者 | 吃点儿好的 [paddle](https://github.com/KateJing1212/community) | +|提交时间 | 2023-03-24 | +|版本号 | V1.0 | +|依赖飞桨版本 | develop | +|文件名 | 20200324_api_design_for_polygamma.md | + + +# 一、概述 +## 1、相关背景 + +对于输入张量,其 digamma 函数的 n 阶导,称为多伽马函数`polygamma`。 + +## 2、功能目标 + +为 Paddle 新增 polygamma API,用于对输入张量的digamma函数进行n阶导操作。 + +## 3、意义 + +为 Paddle 新增 polygamma API,用于对输入张量的digamma函数进行n阶导操作。 + +# 二、飞桨现状 + +飞桨框架目前不支持此功能,但支持 digamma 函数[(参考API文档)](https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/digamma_cn.html) ,且支持输入为张量。digamma 函数是 gamma 函数的对数的一阶导数,k 阶的 polygamma 函数是 gamma 函数的对数的 (k + 1) 阶导数。 + +```Python +def digamma(x, name=None): + r""" + Calculates the digamma of the given input tensor, element-wise. + .. math:: + Out = \Psi(x) = \frac{ \Gamma^{'}(x) }{ \Gamma(x) } + Args: + x (Tensor): Input Tensor. Must be one of the following types: float32, float64. + name (str, optional): Name for the operation (optional, default is None). For more information, please refer to :ref:`api_guide_Name`. + Returns: + Tensor, the digamma of the input Tensor, the shape and data type is the same with input. + Examples: + .. code-block:: python + import paddle + data = paddle.to_tensor([[1, 1.5], [0, -2.2]], dtype='float32') + res = paddle.digamma(data) + print(res) + # Tensor(shape=[2, 2], dtype=float32, place=CUDAPlace(0), stop_gradient=True, + # [[-0.57721591, 0.03648996], + # [ nan , 5.32286835]]) + """ + + if in_dygraph_mode(): + return _C_ops.digamma(x) + else: + if _in_legacy_dygraph(): + return _legacy_C_ops.digamma(x) + + check_variable_and_dtype(x, 'x', ['float32', 'float64'], 'digamma') + helper = LayerHelper('digamma', **locals()) + out = helper.create_variable_for_type_inference(x.dtype) + helper.append_op(type='digamma', inputs={'X': x}, outputs={'Out': out}) + return out +``` + +# 三、业内方案调研 + +## 1. pytorch + +在 pytorch 中使用的 API 格式如下: + +`torch.polygamma(input, n, *, out=None) -> Tensor`[(参考API文档)](https://pytorch.org/docs/stable/generated/torch.polygamma.html) + +上述函数参数中,`input` 是一个Tensor,表示要计算polygamma函数的输入值;`n`是一个整数,表示要计算的polygamma函数的阶数;`out`是一个可选的输出Tensor,用于存储计算结果。 + +实现的代码如下: +```cpp +Tensor polygamma_backward(const Tensor& grad_output, const Tensor& self, int64_t n) { + checkBackend("polygamma_backward", {grad_output, self}, Backend::CPU); + auto input = self; + auto mask = input >= 0; + input = input.abs(); + auto grad_input = at::empty_like(input); + if (input.numel() > 0) { + grad_input.masked_scatter_(mask, at::digamma(input.masked_select(mask)) + at::polygamma(n + 1, input.masked_select(mask))); + grad_input.masked_scatter_(~mask, at::nan(at::dtype(at::kFloat))); + } + return grad_output * grad_input; +} + +Tensor polygamma(const Tensor& self, int64_t n) { + checkBackend("polygamma", {self}, Backend::CPU); + auto result = at::empty_like(self); + AT_DISPATCH_FLOATING_TYPES(self.type(), "polygamma_cpu", [&] { + polygamma_stub::apply(result, self, n); + }); + return result; +} + +``` +可以看到,`polygamma`函数调用了`polygamma_stub`函数来计算`polygamma`函数的值,`polygamma_backward`函数则用于计算梯度。实际的计算是在`polygamma_stub`函数中进行的。完整的`polygamma`函数实现涉及到多个文件和函数。 + +更进一步来说,在CPU上,polygamma在`aten/src/ATen/native/cpu/UnaryOpsKernel.cpp`文件中实现[(参考链接)](https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/native/cpu/UnaryOpsKernel.cpp) +其计算逻辑是通过调用digamma API和trigamma API实现polygamma的计算,具体实现代码如下,: +```cpp +static void polygamma_kernel(TensorIteratorBase& iter, int64_t n) { + if (n == 0) { + digamma_kernel(iter); + } else if (n == 1) { + trigamma_kernel(iter); + } else { + AT_DISPATCH_FLOATING_TYPES_AND(kBFloat16, iter.dtype(), "polygamma", [&]() { + cpu_kernel( + iter, [=](scalar_t a) -> scalar_t { return calc_polygamma(a, n); }); + }); + } +} +``` +另外,引用到的`calc_polygamma`作为辅助函数,其实现代码如下: +```cpp +template +static inline C10_HOST_DEVICE scalar_t calc_polygamma(scalar_t x, int n) { + // already blocked if n <= 1 + const auto one = scalar_t{1}; + return ((n % 2) ? one : -one) * + std::exp(std::lgamma(static_cast(n) + one)) * + zeta(static_cast(n + 1), x); +} +``` + +在CUDA上,polygamma在`aten/src/ATen/native/cpu/UnaryOpsKernel.cpp`文件中实现[(参考链接)](https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/native/cuda/UnaryGammaKernels.cu) +具体实现代码如下: +```cpp +CONSTEXPR_EXCEPT_WIN_CUDA char polygamma_name[] = "polygamma"; +void polygamma_kernel_cuda(TensorIteratorBase& iter, int64_t n) { + if (n == 0) { + digamma_kernel_cuda(iter); + } else if (n == 1) { + trigamma_kernel_cuda(iter); + } else { +#if AT_USE_JITERATOR() + // TODO : `unary_jitted_gpu_kernel` for cleaner UX. + AT_DISPATCH_FLOATING_TYPES_AND_HALF( + iter.common_dtype(), "polygamma_cuda", [&]() { + jitted_gpu_kernel< + /*name=*/polygamma_name, + /*return_dtype=*/scalar_t, + /*common_dtype=*/scalar_t, + /*arity=*/1>( + iter, + polygamma_string, + /*scalar_pos=*/at::cuda::jit::BinaryFuncVariant::NoScalar, + /*scalar_val=*/0, + /*extra_args=*/std::make_tuple(n)); + }); +#else + AT_DISPATCH_FLOATING_TYPES_AND_HALF( + iter.common_dtype(), "polygamma_cuda", [&]() { + gpu_kernel(iter, [=] GPU_LAMBDA(scalar_t a) -> scalar_t { + return calc_polygamma(a, static_cast(n)); + }); + }); +#endif // AT_USE_JITERATOR() + } +} +``` + +## 2. MatLab + +在 MatLab 中使用的 API 格式如下: + +`Y = psi(k,x)`[(参考API文档)](https://ww2.mathworks.cn/help/matlab/ref/psi.html) + +`Y = psi(X)` 为数组 X 的每个元素计算 digamma 函数。而polygamma函数则代表的是在 X 处的 digamma 函数的 k 阶导数。 + +## 3. Tensorflow + +在 Tensorflow 中使用的 API 格式如下: + +`tf.math.polygamma(a, x, name=None)`[(参考API文档)](https://www.tensorflow.org/api_docs/python/tf/math/polygamma) + +上述函数参数中,a是一个非负值的张量;x是一个与a的d类型相同的张量;name定义了该操作的名称。 + +实现的代码如下: + +```cpp +template +class PolygammaOp : public UnaryElementWiseOp> { + public: + using UnaryElementWiseOp>::UnaryElementWiseOp; + + void Operate(OpKernelContext* context, const Tensor& input_tensor, + Tensor* output_tensor) const override { + const auto input = input_tensor.flat(); + auto output = output_tensor->flat(); + + const int32 n = static_cast(this->param_); + const int64 size = input.size(); + + for (int64 i = 0; i < size; ++i) { + output(i) = math::polygamma(n, input(i)); + } + } +}; + +``` + +在该代码中,`PolygammaOp` 继承了 `UnaryElementWiseOp` 类,并实现了 `Operate` 方法,该方法实现了对 `tf.math.polygamma` 函数的操作。 + +## 4. Scipy + +在 Scipy 中使用的 API 格式如下: + +`scipy.special.polygamma(n, x)`[(参考API文档)](https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.polygamma.html) + +其中,`n`表示要计算的 polygamma 函数的阶数;`x`表示 polygamma 函数的自变量。 + +实现的代码如下: + +```cpp +static PyObject *polygamma_wrap(PyObject *self, PyObject *args) +{ + double x; + int n; + double res; + + if (!PyArg_ParseTuple(args, "di:polygamma", &n, &x)) + return NULL; + + if (n == 0) { + res = digamma(x); + } else if (n == 1) { + res = trigamma(x); + } else { + res = psi(n, x); + } + + return PyFloat_FromDouble(res); +} +``` + +在该代码中,`polygamma_wrap` 函数实现了 `scipy.special.polygamma` 函数的底层计算逻辑。当 `n` 等于 0 或 1 时,分别调用 `digamma` 和 `trigamma` 函数计算对应的 polygamma 值。当 `n` 大于 1 时,调用 `psi` 函数计算 n 阶 polygamma 值。 + +# 四、对比分析 + +## 1. 不同框架API使用方式 + +### 1. Scipy + +```Python +import scipy.special + +# 计算 digamma 函数在 x=5 处的值 +x = 5 +n = 0 +result = scipy.special.polygamma(n, x) +print(f"digamma({x}) = {result}") + +# 计算 trigamma 函数在 x=2.5 处的值 +x = 2.5 +n = 1 +result = scipy.special.polygamma(n, x) +print(f"trigamma({x}) = {result}") + +# 计算 pentagamma 函数在 x=3.3+1.2j 处的值 +x = 3.3 + 1.2j +n = 4 +result = scipy.special.polygamma(n, x) +print(f"pentagamma({x}) = {result}") +``` + +### 2. MatLab + +```matlab +n = 2; +x = 3; + +result = psi(n, x); +fprintf("Polygamma value for n = %d and x = %d: %f\n", n, x, result); +``` + +### 3. PyTorch + +```Python +import torch + +n = 2 +x = torch.tensor([1, 2, 3, 4, 5], dtype=torch.float32) + +result = torch.polygamma(n, x) +print("Polygamma values for n = {} and x = {}: {}".format(n, x.numpy(), result.numpy())) +``` + +### 4. Tensorflow + +```Python +import tensorflow as tf + +n = 2 +x = tf.constant([1, 2, 3, 4, 5], dtype=tf.float32) + +result = tf.math.polygamma(n, x) +print("Polygamma values for n = {} and x = {}: {}".format(n, x.numpy(), result.numpy())) +``` + +上述框架从使用体验来说,**Scipy**调用API方便,易于使用,但不支持输入为张量。**MatLab**中的polygamma值计算与digamma复用同一函数,实现逻辑相似但不支持输入张量。**PyTorch**和**Tensorflow** 支持输入张量,输出结果为一个 TensorFlow 的`tf.Tensor` 对象,如有需要可以借助其他方法转换为python标准类型。 + +# 五、设计思路与实现方案 + +## 命名与参数设计 + + + +API设计为 `paddle.polygamma(x, n, name=None)`。 +`x`为张量,允许的数据类型是float32和float64,其原因是digamma对数据类型进行了限制。 +`n`表示多项式 gamma 函数的导数阶数,其可以为一个非负整数或一个非负值张量,允许的数据类型是int32和int64;如果`n`是一个张量,需要保证`n`的维度`ndim`和`n`的参数量`numel`满足`ndim<=1`且`numel==1`;当`n=0`时,polygamma退化为 digamma。 +`name`作为可选参数,定义了该操作的名称,其默认值为`None`。 +另外,该API还支持`Tensor.polygamma(n)`的调用形式。 + +## 底层OP设计 + +polygamma可基于现有API即digamma进行实现,不再单独设计OP。 + +## API实现方案 + +该 API 实现于 `python/paddle/tensor/math.py`,通过调研发现,Paddle 本身已实现 paddle.digamma,可以计算gamma 函数的对数的一阶导数,可利用paddle.digamma API 做n阶导实现 paddle.polygamma。 +具体来说,polygamma函数的k阶定义为: +``` +polygamma(k, x) = d^k / dx^k [ln(gamma(x))] +``` +根据导数的链式法则,可以将上述公式递归表示为: +``` +polygamma(k, x) = polygamma(1, polygamma(k-1, x)) +``` +因此,初步设计的polygamma实现伪代码如下: +``` +def digamma(x): + # Digamma API implementation + ... + +def polygamma(x, k): + if k == 1: + return digamma(x) + else: + return (-1) ** (k - 1) * factorial(k - 1) * polygamma(x, k - 1) + digamma(x) +``` + +另外,由于该API需要考虑动静统一问题,故需要验证其在静态图中能否正常工作。 + +# 六、测试和验收的考量 + + + +可考虑以下场景: + +### 1. 一般场景 +* 结果一致性测试。测试对于同一输入和 Tensorflow 以及 PyTorch 中polygamma API计算结果的数值的一致性。 +* 数据类型测试。选取不同数据类型的输入,测试计算结果的准确性。 +* 参数取值测试。选取不同取值的参数 `k` (表示求导的阶数),测试计算结果的准确性。 +### 2. 边界条件 +* 当 `x` 为空张量,测试其输出是否空张量且输出张量形状是否正确。 +* 当 `n=0` ,测试其输出是否与digamma API得到的计算结果相同。 +### 3. 异常测试 +* 对于参数异常值输入,例如x的不合法值等,应该有友好的报错信息及异常反馈,需要有相关测试Case验证。 + +# 七、可行性分析和排期规划 + +本 API 主要参考已有API实现,难度适中,工期上能满足要求。 + +# 八、影响面 + +为独立新增API,对其他模块没有影响。 + +# 名词解释 + +# 附件及参考资料