From a574d378d4c272fbd078c2800f66bb7161fdc564 Mon Sep 17 00:00:00 2001
From: thunder95 <290844930@qq.com>
Date: Mon, 24 Apr 2023 17:00:09 +0800
Subject: [PATCH] =?UTF-8?q?=E3=80=90Hackathon4=20No.26=E3=80=91=E4=B8=BA?=
=?UTF-8?q?=20Paddle=20=E6=96=B0=E5=A2=9E=20paddle.sparse.nn.Softmax=20?=
=?UTF-8?q?=E7=A8=80=E7=96=8F=20API=20=E7=9A=84=20coo=20=E6=A0=BC=E5=BC=8F?=
=?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91=20(#514)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* erfinv
* false commit
* erfinv
* false commit
* ass softmax coo
* add cuda kernel design
---
...19_api_design_for_sparse_coo_nn_softmax.md | 337 ++++++++++++++++++
1 file changed, 337 insertions(+)
create mode 100644 rfcs/APIs/20230419_api_design_for_sparse_coo_nn_softmax.md
diff --git a/rfcs/APIs/20230419_api_design_for_sparse_coo_nn_softmax.md b/rfcs/APIs/20230419_api_design_for_sparse_coo_nn_softmax.md
new file mode 100644
index 000000000..c7c7b50ae
--- /dev/null
+++ b/rfcs/APIs/20230419_api_design_for_sparse_coo_nn_softmax.md
@@ -0,0 +1,337 @@
+# paddle.sparse.nn.Softmax 设计文档
+
+| API名称 | paddle.sparse.nn.Softmax |
+| ------------------------------------------------------------ | ------------------------------------------------- |
+| 提交作者 | thunder95 |
+| 提交时间 | 2023-04-19 |
+| 版本号 | V1.0 |
+| 依赖飞桨版本 | Develop |
+| 文件名 | 20230419_api_design_for_sparse_coo_nn_softmax.md
|
+
+
+# 一、概述
+
+## 1、相关背景
+
+稀疏 Tensor 是元素大部分为零的矩阵,在实际求解任务时经常出现大规模的稀疏 Tensor。由于其自身的稀疏性,为了节省存储空间,通常会修改稀疏 Tensor 的存储结构。目前比较普遍的存储结构为 COO 和 CSR。
+
+Paddle 目前已经实现了 COO 和 CSR 格式的稀疏 Tensor 的构建以及一些算子操作,Softmax目前仅仅支持了 CSR 格式的稀疏 Tensor, 还需要对COO格式的支持。
+
+## 2、功能目标
+
+在飞桨中增加 paddle.sparse.nn.Softmax 对COO稀疏格式的支持。
+
+## 3、意义
+
+飞桨将支持 paddle.sparse.nn.Softmax 在coo 稀疏格式下的计算逻辑。
+
+# 二、飞桨现状
+
+目前飞桨的paddle.sparse.nn.Softmax API 仅支持CSR 格式, 还不支持COO稀疏格式。
+
+
+# 三、业内方案调研
+
+## TensorFlow
+
+Tensorflow中提供了softmax稀疏算子支持, 详情可参考官方文档([tf.sparse.softmax](https://tensorflow.google.cn/api_docs/python/tf/sparse/softmax)) 。
+
+``` python
+tf.sparse.softmax(
+ sp_input, name=None
+)
+```
+具体核心实现代码如下所示(截取自 [tensorflow/core/kernels/sparse_softmax_op.cc](https://github.com/tensorflow/tensorflow/blob/v2.12.0/tensorflow/core/kernels/sparse_softmax_op.cc)
+```cpp
+template
+class SparseSoftmaxOp : public OpKernel {
+ public:
+ explicit SparseSoftmaxOp(OpKernelConstruction *context) : OpKernel(context) {}
+
+ void Compute(OpKernelContext *context) override {
+ const Tensor *indices_t, *values_t, *shape_t;
+ OP_REQUIRES_OK(context, context->input("sp_indices", &indices_t));
+ OP_REQUIRES_OK(context, context->input("sp_values", &values_t));
+ OP_REQUIRES_OK(context, context->input("sp_shape", &shape_t));
+
+ // Validations.
+ OP_REQUIRES(context, TensorShapeUtils::IsMatrix(indices_t->shape()),
+ errors::InvalidArgument(
+ "Input sp_indices should be a matrix but received shape: ",
+ indices_t->shape().DebugString()));
+ OP_REQUIRES(context,
+ TensorShapeUtils::IsVector(values_t->shape()) &&
+ TensorShapeUtils::IsVector(shape_t->shape()),
+ errors::InvalidArgument(
+ "Inputs sp_values and sp_shape should be vectors "
+ "but received shapes: ",
+ values_t->shape().DebugString(), " and ",
+ shape_t->shape().DebugString()));
+ OP_REQUIRES(context, shape_t->NumElements() >= 2,
+ errors::InvalidArgument(
+ "Input should have rank >= 2, but received shape: ",
+ shape_t->SummarizeValue(3)));
+ TensorShape shape;
+ OP_REQUIRES_OK(context, TensorShape::BuildTensorShape(
+ shape_t->flat(), &shape));
+
+ const int64_t nnz = indices_t->dim_size(0);
+ const int rank = static_cast(indices_t->dim_size(1));
+ SparseTensor st;
+ OP_REQUIRES_OK(
+ context, SparseTensor::Create(tensor::DeepCopy(*indices_t),
+ tensor::DeepCopy(*values_t), shape, &st));
+
+ Tensor *output_values = nullptr;
+ OP_REQUIRES_OK(context, context->allocate_output(0, TensorShape({nnz}),
+ &output_values));
+ typename TTypes::Flat output_flat = output_values->flat();
+
+ Tensor tmp_t;
+ OP_REQUIRES_OK(context, context->allocate_temp(DataTypeToEnum::value,
+ TensorShape({}), &tmp_t));
+ typename TTypes::Scalar tmp_scalar = tmp_t.scalar();
+
+ gtl::InlinedVector dims(rank);
+ std::iota(dims.begin(), dims.end(), 0);
+ // { 0, ..., rank-1 }.
+ const ArraySlice kReorderDims(dims);
+ // All but the last dim -- the class dimension to be max-reduced along.
+ const ArraySlice kGroupByDims = kReorderDims.subspan(0, rank - 1);
+ st.Reorder(kReorderDims);
+ int count = 0;
+
+ // The SparseTensor has logical shape [..., b, c], where the
+ // innermost size-"c" dimension is the class dimension to be max-reduced.
+ // Therefore we group by the first (rank - 1) dimensions.
+ const Device &device = context->eigen_device();
+ for (const auto &g : st.group(kGroupByDims)) {
+ const auto group_vals = g.values();
+ const int group_size = group_vals.size();
+
+ // Shifts by max, exponentiates, then renormalizes.
+ tmp_scalar.device(context->eigen_device()) = group_vals.maximum();
+ const T group_max = tmp_scalar();
+
+ Eigen::Tensor tmp(group_size);
+ tmp.device(device) = (group_vals - tmp.constant(group_max)).exp();
+
+ tmp_scalar.device(device) = tmp.sum().inverse();
+ tmp.device(device) = tmp * tmp.constant(tmp_scalar());
+
+ // Assigns back to output[count, count + group_size).
+ Eigen::TensorMap> output_part(
+ output_flat.data() + count, group_size);
+ output_part.device(device) = tmp;
+
+ count += group_size;
+ }
+ }
+};
+```
+
+## SciPy
+
+SciPy 不支持对稀疏 Tensor 的 softmax 操作。
+
+## Pytorch
+
+Pytorch中支持了softmax API的COO格式稀疏算子, 详情可参考官方文档([torch.sparse.softmax](https://pytorch.org/docs/stable/generated/torch.sparse.softmax.html) 。
+具体核心实现代码如下所示(截取自 [pytorch/src/ATen/native/sparse/SoftMax.cpp](https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/sparse/SoftMax.cpp)
+
+``` cpp
+template
+void cpu_sparse_coo_softmax(Tensor output, const Tensor& input, const int64_t dim) {
+ auto sparse_dim = input.sparse_dim();
+ auto indices = input._indices().contiguous();
+ auto values = input._values().contiguous();
+ auto out_values = output._values();
+ auto out_indices = output._indices();
+ out_values.resize_as_(values);
+ out_indices.resize_as_(indices);
+ out_indices.copy_(indices);
+
+ if (dim >= sparse_dim) {
+ if (LogSoftMax) {
+ auto new_values =
+ at::cpu::_log_softmax(values, dim - sparse_dim + 1, false);
+ out_values.set_(new_values);
+ } else {
+ auto new_values = at::cpu::_softmax(values, dim - sparse_dim + 1, false);
+ out_values.set_(new_values);
+ }
+ return;
+ }
+
+ auto nnz = values.size(0);
+ auto sizes = input.sizes();
+ auto nvalues = get_nvalues(sizes, sparse_dim);
+
+ /* Prepare accessors */
+ auto values_2 = values.view({nnz, nvalues});
+ auto values_accessor = values_2.accessor();
+
+ auto out_values_2 = out_values.view({nnz, nvalues});
+ auto out_values_accessor = out_values_2.accessor();
+
+ /* Compute independent pools of indices */
+ auto pools = get_pools(indices, sizes, dim);
+
+ int64_t grain_size = 1;
+ parallel_for(0, pools.size(), grain_size, [&](int64_t begin, int64_t end) {
+ for (const auto p : c10::irange(begin, end)) {
+ auto pool_indices = pools[p];
+
+ // Skip empty pools
+ if (pool_indices.empty())
+ continue;
+
+ /* Prepare scratch space */
+ std::vector mx_row(nvalues, -std::numeric_limits::infinity());
+ std::vector exp_sums_row(nvalues, 0);
+
+ /* Compute mx */
+ for (int64_t i : pool_indices) {
+ auto values_row = values_accessor[i];
+ for (const auto j : c10::irange(nvalues)) {
+ mx_row[j] = std::max(mx_row[j], values_row[j]);
+ }
+ }
+
+ /* Apply exp to (v - mx) and sum the results */
+ for (int64_t i : pool_indices) {
+ auto values_row = values_accessor[i];
+ auto out_values_row = out_values_accessor[i];
+ for (const auto j : c10::irange(nvalues)) {
+ auto v = std::exp(values_row[j] - mx_row[j]);
+ if (!LogSoftMax) {
+ out_values_row[j] = v;
+ }
+ exp_sums_row[j] += v;
+ }
+ }
+
+ for (const auto j : c10::irange(nvalues)) {
+ if (LogSoftMax) {
+ mx_row[j] += std::log(exp_sums_row[j]);
+ } else {
+ exp_sums_row[j] = 1.0 / exp_sums_row[j];
+ }
+ }
+
+ /* Normalize with the sum of exponents */
+ for (int64_t i : pool_indices) {
+ auto values_row = values_accessor[i];
+ auto out_values_row = out_values_accessor[i];
+ for (const auto j : c10::irange(nvalues)) {
+ if (LogSoftMax) {
+ out_values_row[j] = values_row[j] - mx_row[j];
+ } else {
+ out_values_row[j] *= exp_sums_row[j];
+ }
+ }
+ }
+ }
+ });
+}
+```
+
+# 四、对比分析
+
+Tensorflow基于Eigen计算,支持COO稀疏格式,不支持axis传入。
+Scipy没有直接支持softmax的稀疏算子计算。
+Pytorch中能支持axis传入,且支持COO格式的稀疏算子。
+
+
+# 五、设计思路与实现方案
+
+## 命名与参数设计
+
+sparse softmax 已经支持 CSR 格式,这个稀疏张量上的方法的命名和参数不需要额外设计,只需要添加相应的COO格式支持。
+
+在 paddle/phi/api/yaml 下新增注册该算子COO格式的前向以及反向。
+
+``` yaml
+- op : softmax
+ args : (Tensor x, int axis=-1)
+ output : Tensor(out)
+ infer_meta :
+ func : UnchangedInferMeta
+ param : [x]
+ kernel :
+ func : softmax_coo{sparse_coo -> sparse_coo},
+ softmax_csr{sparse_csr -> sparse_csr}
+ layout : x
+ backward : softmax_grad
+```
+
+
+``` yaml
+- backward_op : softmax_grad
+ forward : softmax(Tensor x, int axis=-1) -> Tensor(out)
+ args : (Tensor out, Tensor out_grad, int axis)
+ output : Tensor(x_grad)
+ infer_meta :
+ func : UnchangedInferMeta
+ param : [out]
+ kernel :
+ func : softmax_coo_grad{sparse_coo, sparse_coo -> sparse_coo},
+ softmax_csr_grad{sparse_csr, sparse_csr -> sparse_csr}
+```
+
+## 底层OP设计
+
+新增一个COO格式的前向以及反向Kernel:
+
+``` cpp
+template
+void SoftmaxCooKernel(const Context& dev_ctx,
+ const SparseCooTensor& x,
+ int axis,
+ SparseCooTensor* out);
+```
+
+``` cpp
+template
+void SoftmaxCooGradKernel(const Context& dev_ctx,
+ const SparseCooTensor& out,
+ const SparseCooTensor& dout,
+ int axis,
+ SparseCooTensor* dx);
+```
+
+## API实现方案
+
+在python/paddle/sparse/nn/functional/activation.py 文件和 python/paddle/sparse/nn/layer/activation.py 文件中的原API上没有改动。
+
+参考pytorch的计算方式,先计算索引的pool映射,在对应维度上一次计算max以及求和,最终对指数的求和进行normalize计算。
+
+在cuda的kernel中, 若指定的axis大于等于稀疏维度,将使用稠密张量的softmax算子,若小于则分两步;
+- 先计算pool和max, 基于Thrust库设计函数ComputePoolMax, 计算出指定维度上索引的pools以及每个pool对应的最大值,
+- 基于pool的数量,设计对应的block和grid,调用SparseCooSoftmaxKernel, 计算pool内的softmax值
+
+在反向梯度SparseCooSoftmaxGradKernel计算中,需先设计函数GetOffsets, 基于稀疏张量的索引计算对应稠密张量的偏移量,进而通过反向求导的公式计算梯度。
+
+
+# 六、测试和验收的考量
+
+完善单测代码,python/paddle/fluid/tests/unittests/test_sparse_softmax_op.py 文件中新增测试COO稀疏格式的case如下:
+
+- 数值正确性
+- COO数据格式
+- 不同输入tensor的数据类型下检查输出结果
+- 计算结果与dense tensor进行比较
+
+# 七、可行性分析和排期规划
+
+前两周实现代码、文档和测试。
+
+第三周进行 Code Review 和继续迭代。
+
+# 八、影响面
+
+对其它模块没有影响。
+
+# 名词解释
+
+# 附件及参考资料