Files
paddlepaddle--paddle/paddle/phi/kernels/funcs/gather_scatter_functor.cc
T
2026-07-13 12:40:42 +08:00

745 lines
27 KiB
C++

/* 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. */
#include "paddle/phi/kernels/funcs/gather_scatter_functor.h"
#include "glog/logging.h"
#include "paddle/common/macros.h"
#include "paddle/phi/kernels/funcs/math_function.h"
namespace phi::funcs {
class TensorAssign {
public:
template <typename tensor_t>
void operator()(tensor_t* self_data, tensor_t* src_data) const {
*self_data = *src_data;
}
};
static TensorAssign tensor_assign;
class ReduceAdd {
public:
template <typename tensor_t>
void operator()(tensor_t* self_data, tensor_t* src_data) const {
*self_data += *src_data;
}
};
static ReduceAdd reduce_add;
class ReduceMultiply {
public:
template <typename tensor_t>
void operator()(tensor_t* self_data, tensor_t* src_data) const {
*self_data *= *src_data;
}
};
static ReduceMultiply reduce_mul;
class ReduceMax {
public:
template <typename tensor_t>
void operator()(tensor_t* self_data, tensor_t* src_data) const {
*self_data = *src_data > *self_data ? *src_data : *self_data;
}
};
static ReduceMax reduce_max;
class ReduceMin {
public:
template <typename tensor_t>
void operator()(tensor_t* self_data, tensor_t* src_data) const {
*self_data = *src_data < *self_data ? *src_data : *self_data;
}
};
static ReduceMin reduce_min;
template <typename T>
inline T IntFloorDiv(T a, T b) {
if ((a < 0) != (b < 0)) {
// compute div and mod at the same time can be optimized by compilers
const auto quot = a / b;
const auto rem = a % b;
return rem ? quot - 1 : quot;
}
return a / b;
}
/**
* A divmod free solution for faster offset mapping. This class only do the
* necessary multiplication, therefore the computation and memory access should
* be lower than divmod and naive index mapping. Usage:
*
* \code
* CoordinateManager<true> cm(index_shape, self_strides, ndim,
* axis_to_put, &src_strides);
*
* for (int i = 0; i < index_shape.numel(); i++) {
* index_t index = index_data[i];
* cm.CalculateOffset(index_t);
* int64_t replace_self_index = cm.offset1;
* int64_t replace_src_index = cm.offset2;
* ...
* }
* \endcode
*/
template <bool compute_both = false>
class CoordinateManager {
private:
const DDim& shape;
const DDim& strides1;
const int ndim;
const int src_dim;
int64_t last_offset;
std::vector<int64_t> indices;
const DDim* strides2;
public:
int64_t offset1;
int64_t offset2;
CoordinateManager(const DDim& _shape,
const DDim& _strides1,
int _ndim,
int _src_dim,
const DDim* _strides2 = nullptr)
: shape(_shape),
strides1(_strides1),
ndim(_ndim),
src_dim(_src_dim),
last_offset(0),
strides2(_strides2),
offset1(0),
offset2(0) {
indices.resize(ndim, 0);
// calculate correct starting offsets
if (ndim - 1 != _src_dim) offset1 = -strides1[ndim - 1];
if constexpr (compute_both) offset2 = -strides2->operator[](ndim - 1);
}
template <typename index_t>
void CalculateOffset(index_t index) {
int change_dim = ndim - 1;
// step 1: calculate the carry or borrow dim
for (int dim = ndim - 1; dim > 0; dim--) {
if (indices[dim] >= shape[dim]) {
indices[dim] = 0;
change_dim = dim - 1;
// carry or borrow operation: we do not check boundaries here, please
// make sure that do not call map_offset more than index.numel(),
// otherwise we will have illegal access
++indices[change_dim];
}
}
// step 2: update the axis to put/take offset
offset1 -= last_offset;
last_offset = index * strides1[src_dim];
offset1 += last_offset;
// step 3: clear the offset due to carry using minimum number of `mul`s.
// skip all src_dim related computation, since they have independent
// logics. Also, if strides2 (compute both) is available, compute the
// offset (usually for src tensor).
if (change_dim != src_dim) offset1 += strides1[change_dim];
if constexpr (compute_both) offset2 += strides2->operator[](change_dim);
for (int dim = change_dim + 1; dim < ndim; dim++) {
int64_t dim_max_index = shape[dim] - 1;
// clear the tail elements after the carrying dim
if constexpr (compute_both)
offset2 -= strides2->operator[](dim) * dim_max_index;
if (dim == src_dim) continue;
offset1 -= strides1[dim] * dim_max_index;
}
++indices.back();
}
};
/**
* Used in some of the value grad calculation, since those compute indices in a
* back-to-front order. Decide not to fuse with CoordinateManager via
* templating, otherwise the readability will be bad.
*/
template <bool compute_both = false>
class ReversedCoordinateManager {
private:
const DDim& shape;
const DDim& strides1;
const int ndim;
const int src_dim;
int64_t last_offset;
std::vector<int64_t> indices;
const DDim* strides2;
public:
int64_t offset1;
int64_t offset2;
ReversedCoordinateManager(const DDim& _shape,
const DDim& _strides1,
int _ndim,
int _src_dim,
const DDim* _strides2 = nullptr)
: shape(_shape),
strides1(_strides1),
ndim(_ndim),
src_dim(_src_dim),
last_offset(0),
strides2(_strides2),
offset1(0),
offset2(0) {
indices.resize(ndim, 0);
// reversed should have an extra stride.back()
if (ndim - 1 != _src_dim) offset1 = strides1[ndim - 1];
if constexpr (compute_both) offset2 = strides2->operator[](ndim - 1);
for (int i = 0; i < _ndim; i++) {
indices[i] = _shape[i] - 1;
if constexpr (compute_both)
offset2 += strides2->operator[](i) * indices[i];
if (i == src_dim) continue;
offset1 += strides1[i] * indices[i];
}
}
template <typename index_t>
void CalculateOffset(index_t index) {
int change_dim = ndim - 1;
// step 1: calculate the borrow dim
for (int dim = ndim - 1; dim > 0; dim--) {
if (indices[dim] < 0) {
indices[dim] = shape[dim] - 1;
change_dim = dim - 1;
--indices[change_dim];
}
}
// step 2: update the axis to put/take offset
offset1 -= last_offset;
last_offset = index * strides1[src_dim];
offset1 += last_offset;
// step 3: clear the offset due to borrow using minimum number of `mul`s.
if (change_dim != src_dim) offset1 -= strides1[change_dim];
if constexpr (compute_both) offset2 -= strides2->operator[](change_dim);
for (int dim = change_dim + 1; dim < ndim; dim++) {
int64_t dim_max_index = shape[dim] - 1;
// clear the tail elements after the carrying dim
if constexpr (compute_both)
offset2 += strides2->operator[](dim) * dim_max_index;
if (dim == src_dim) continue;
offset1 += strides1[dim] * dim_max_index;
}
--indices.back();
}
};
template <typename tensor_t,
typename index_t = int64_t,
bool is_scatter_like = true>
struct cpu_gather_scatter_functor {
template <typename func_t>
void operator()(DenseTensor self,
int dim,
const DenseTensor& index,
const DenseTensor& src,
const std::string& method_name,
const func_t& reduce_op,
bool include_self,
const DeviceContext& dev_ctx UNUSED) {
if (index.numel() == 0) {
return;
}
auto* self_data = self.data<tensor_t>();
auto* index_data = index.data<index_t>();
auto* src_data = src.data<tensor_t>();
int64_t self_size = self.numel();
int64_t index_size = index.numel();
int64_t src_size = src.numel();
auto self_dims = self.dims();
auto src_dims = src.dims();
const bool is_gather_or_scatter_assign =
method_name == "gather" || method_name == "assign";
if (self_size == 0 || src_size == 0 || index_size == 0) {
VLOG(3) << "zero size input found";
common::errors::InvalidArgument(
"self_size, src_size, index_size cannot be 0");
return;
}
int64_t self_select_dim_size = self_dims[dim];
int64_t src_select_dim_size = src_dims[dim];
// gather and assign do not need nums_of_elements
std::vector<int64_t> nums_of_elements;
if (!is_gather_or_scatter_assign) nums_of_elements.resize(self.numel(), 0);
const int ndim = index.dims().size();
CoordinateManager<is_scatter_like> cm(
index.dims(),
is_scatter_like ? self.strides() : src.strides(),
ndim,
dim,
&src.strides());
for (int64_t i = 0; i < index_size; i++) {
int64_t index = index_data[i];
int64_t replace_index_self = 0, replace_index_src = 0;
// offset1 is always related to index
if constexpr (is_scatter_like) {
PADDLE_ENFORCE_EQ(
(index >= -self_select_dim_size) && (index < self_select_dim_size),
true,
common::errors::OutOfRange(
"Variable value (index) of scatter cpu kernel, "
"expected >= %d and < %d, but got %ld."
"Please check the input value.",
-self_select_dim_size,
self_select_dim_size,
index));
if (index < 0) index += self_select_dim_size;
cm.CalculateOffset(index);
replace_index_self = cm.offset1;
replace_index_src = cm.offset2;
} else {
PADDLE_ENFORCE_EQ(
(index >= -src_select_dim_size) && (index < src_select_dim_size),
true,
common::errors::OutOfRange(
"Variable value (index) of gather cpu kernel, "
"expected >= %d and < %d, but got %ld."
"Please check the input value.",
-src_select_dim_size,
src_select_dim_size,
index));
if (index < 0) index += src_select_dim_size;
cm.CalculateOffset(index);
replace_index_self = i;
replace_index_src = cm.offset1;
}
if (include_self == false && is_gather_or_scatter_assign == false &&
nums_of_elements[replace_index_self] == 0) {
self_data[replace_index_self] = src_data[replace_index_src];
} else {
reduce_op((tensor_t*)(self_data + replace_index_self), // NOLINT
(tensor_t*)(src_data + replace_index_src)); // NOLINT
}
if (!is_gather_or_scatter_assign)
nums_of_elements[replace_index_self] += 1;
}
if (method_name == "mean") {
if (include_self) {
for (int64_t i = 0; i < self_size; i++) {
if (!nums_of_elements[i]) continue;
if constexpr (std::is_integral_v<std::decay_t<tensor_t>>) {
self_data[i] = IntFloorDiv(
self_data[i], static_cast<tensor_t>(nums_of_elements[i] + 1));
} else {
self_data[i] =
self_data[i] / static_cast<tensor_t>(nums_of_elements[i] + 1);
}
}
} else {
for (int64_t i = 0; i < self_size; i++) {
if (!nums_of_elements[i]) continue;
if constexpr (std::is_integral_v<std::decay_t<tensor_t>>) {
self_data[i] = IntFloorDiv(
self_data[i], static_cast<tensor_t>(nums_of_elements[i]));
} else {
self_data[i] =
self_data[i] / static_cast<tensor_t>(nums_of_elements[i]);
}
}
}
}
}
};
template <typename tensor_t, typename index_t>
void cpu_gather_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
DenseTensor result,
bool include_self,
const DeviceContext& dev_ctx) {
cpu_gather_scatter_functor<tensor_t,
index_t,
/*is_scatter_like=*/false>()(
result, dim, index, self, "gather", tensor_assign, include_self, dev_ctx);
}
template <typename tensor_t, typename index_t>
void cpu_scatter_assign_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
DenseTensor src,
bool include_self,
const DeviceContext& dev_ctx) {
cpu_gather_scatter_functor<tensor_t,
index_t,
/*is_scatter_like=*/true>()(
self, dim, index, src, "assign", tensor_assign, include_self, dev_ctx);
}
template <typename tensor_t, typename index_t>
void cpu_scatter_add_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
DenseTensor src,
bool include_self,
const DeviceContext& dev_ctx) {
cpu_gather_scatter_functor<tensor_t,
index_t,
/*is_scatter_like=*/true>()(
self, dim, index, src, "add", reduce_add, include_self, dev_ctx);
}
template <typename tensor_t, typename index_t>
void cpu_scatter_mul_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
DenseTensor src,
bool include_self,
const DeviceContext& dev_ctx) {
cpu_gather_scatter_functor<tensor_t,
index_t,
/*is_scatter_like=*/true>()(
self, dim, index, src, "mul", reduce_mul, include_self, dev_ctx);
}
template <typename tensor_t, typename index_t>
void cpu_scatter_mean_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
DenseTensor src,
bool include_self,
const DeviceContext& dev_ctx) {
cpu_gather_scatter_functor<tensor_t,
index_t,
/*is_scatter_like=*/true>()(
self, dim, index, src, "mean", reduce_add, include_self, dev_ctx);
}
template <typename tensor_t, typename index_t>
void cpu_scatter_max_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
DenseTensor src,
bool include_self,
const DeviceContext& dev_ctx) {
cpu_gather_scatter_functor<tensor_t,
index_t,
/*is_scatter_like=*/true>()(
self, dim, index, src, "max", reduce_max, include_self, dev_ctx);
}
template <typename tensor_t, typename index_t>
void cpu_scatter_min_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
DenseTensor src,
bool include_self,
const DeviceContext& dev_ctx) {
cpu_gather_scatter_functor<tensor_t,
index_t,
/*is_scatter_like=*/true>()(
self, dim, index, src, "min", reduce_min, include_self, dev_ctx);
}
template <typename tensor_t, typename index_t>
void cpu_scatter_input_grad_kernel(DenseTensor self UNUSED,
int dim,
const DenseTensor& index,
DenseTensor grad,
bool include_self UNUSED,
const DeviceContext& dev_ctx UNUSED) {
auto* index_data = index.data<index_t>();
auto* grad_data = grad.data<tensor_t>();
const int ndim = index.dims().size();
const int64_t index_size = index.numel();
CoordinateManager<false> cm(index.dims(), grad.strides(), ndim, dim, nullptr);
for (int64_t i = 0; i < index_size; i++) {
int64_t index = index_data[i];
cm.CalculateOffset(index);
int64_t replace_index = cm.offset1;
grad_data[replace_index] = 0;
}
}
template <typename tensor_t, typename index_t>
void cpu_scatter_mul_min_max_input_grad_kernel(DenseTensor self UNUSED,
int dim,
const DenseTensor& index,
const DenseTensor& out,
const DenseTensor& x,
const DenseTensor& value,
DenseTensor grad,
const std::string& reduce,
bool include_self UNUSED,
const DeviceContext& dev_ctx) {
auto* index_data = index.data<index_t>();
auto* grad_data = grad.data<tensor_t>();
auto* out_data = out.data<tensor_t>();
auto* x_data = x.data<tensor_t>();
auto* value_data = value.data<tensor_t>();
const int ndim = index.dims().size();
const int64_t index_size = index.numel();
const int64_t grad_size = grad.numel();
// only amin/amax needs the offset2, but we compute together anyway.
CoordinateManager<true> cm(
index.dims(), grad.strides(), ndim, dim, &value.strides());
// make sure that reduce in {'mul', 'multiply', 'amin', 'amax'}
const bool is_mul = reduce == "multiply" || reduce == "mul";
std::vector<int> num_elements(grad.numel(), 0);
for (int64_t i = 0; i < index_size; i++) {
int64_t index = index_data[i];
cm.CalculateOffset(index);
int64_t replace_index_grad = cm.offset1;
if (is_mul && num_elements[replace_index_grad] == 0) {
if (x_data[replace_index_grad] != static_cast<tensor_t>(0)) {
tensor_t val = grad_data[replace_index_grad];
val *= out_data[replace_index_grad];
val /= x_data[replace_index_grad];
grad_data[replace_index_grad] = static_cast<tensor_t>(val);
} else {
grad_data[replace_index_grad] = static_cast<tensor_t>(0);
}
num_elements[replace_index_grad] += 1;
} else if (!is_mul) {
if (out_data[replace_index_grad] != x_data[replace_index_grad]) {
grad_data[replace_index_grad] = 0;
} else {
int64_t replace_index_value = cm.offset2;
if (out_data[replace_index_grad] == value_data[replace_index_value])
num_elements[replace_index_grad] += 1;
}
}
}
// TODO(heqianyue): I don't think the origin impl is correct, what about
// include_self = False?
if (!is_mul) {
for (int64_t i = 0; i < grad_size; i++) {
grad_data[i] = grad_data[i] / static_cast<tensor_t>(num_elements[i] + 1);
}
}
}
template <typename tensor_t, typename index_t>
void cpu_scatter_mean_input_grad_kernel(DenseTensor self UNUSED,
int dim,
const DenseTensor& index,
DenseTensor grad,
bool include_self UNUSED,
const DeviceContext& dev_ctx UNUSED) {
auto* index_data = index.data<index_t>();
auto* grad_data = grad.data<tensor_t>();
int64_t grad_size = grad.numel();
const int ndim = index.dims().size();
const int64_t index_size = index.numel();
CoordinateManager<false> cm(index.dims(), grad.strides(), ndim, dim, nullptr);
std::vector<int> num_elements(grad_size, 0);
for (int64_t i = 0; i < index_size; i++) {
int64_t index = index_data[i];
cm.CalculateOffset(index);
int64_t replace_index = cm.offset1;
num_elements[replace_index] += 1;
}
for (int64_t i = 0; i < grad_size; i++)
if (num_elements[i])
grad_data[i] = grad_data[i] / static_cast<tensor_t>(num_elements[i] + 1);
}
template <typename tensor_t, typename index_t>
void cpu_scatter_value_grad_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
DenseTensor grad,
bool include_self UNUSED,
const DeviceContext& dev_ctx UNUSED) {
const auto* self_data = self.data<tensor_t>();
auto* index_data = index.data<index_t>();
auto* grad_data = grad.data<tensor_t>();
std::vector<bool> is_self_grad_used(self.numel(), false);
const int ndim = index.dims().size();
ReversedCoordinateManager<true> cm(
index.dims(), self.strides(), ndim, dim, &grad.strides());
for (int64_t i = index.numel() - 1; i >= 0; i--) {
int64_t index = index_data[i];
cm.CalculateOffset(index);
int64_t replace_index_self = cm.offset1;
int64_t replace_index_grad = cm.offset2;
if (!is_self_grad_used[replace_index_self]) {
grad_data[replace_index_grad] = self_data[replace_index_self];
is_self_grad_used[replace_index_self] = true;
}
}
}
template <typename tensor_t, typename index_t>
void cpu_scatter_add_mean_value_grad_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
const DenseTensor& out UNUSED,
const DenseTensor& x UNUSED,
const DenseTensor& value UNUSED,
DenseTensor grad,
const std::string& reduce,
bool include_self,
const DeviceContext& dev_ctx) {
const auto* self_data = self.data<tensor_t>();
auto* index_data = index.data<index_t>();
auto* grad_data = grad.data<tensor_t>();
int64_t self_size = self.numel();
funcs::set_constant(dev_ctx, &grad, 0);
std::vector<int> num_elements;
const int ndim = index.dims().size();
// Note: make sure that `reduce` in {'mean', 'add'}.
const bool is_mean = reduce == "mean";
if (is_mean) {
num_elements.resize(self_size, static_cast<int>(include_self));
ReversedCoordinateManager<false> cm(
index.dims(), self.strides(), ndim, dim, nullptr);
for (int64_t i = index.numel() - 1; i >= 0; i--) {
int64_t index = index_data[i];
cm.CalculateOffset(index);
int64_t replace_index_self = cm.offset1;
num_elements[replace_index_self] += 1;
}
}
ReversedCoordinateManager<true> cm(
index.dims(), self.strides(), ndim, dim, &grad.strides());
for (int64_t i = index.numel() - 1; i >= 0; i--) {
int64_t index = index_data[i];
cm.CalculateOffset(index);
int64_t replace_index_self = cm.offset1;
int64_t replace_index_grad = cm.offset2;
if (is_mean) {
grad_data[replace_index_grad] =
self_data[replace_index_self] /
static_cast<tensor_t>(num_elements[replace_index_self]);
} else {
grad_data[replace_index_grad] = self_data[replace_index_self];
}
}
}
template <typename tensor_t, typename index_t>
void cpu_scatter_mul_min_max_value_grad_kernel(DenseTensor self,
int dim,
const DenseTensor& index,
const DenseTensor& out,
const DenseTensor& x,
const DenseTensor& value,
DenseTensor grad,
const std::string& reduce,
bool include_self,
const DeviceContext& dev_ctx) {
const auto* self_data = self.data<tensor_t>();
auto* index_data = index.data<index_t>();
auto* grad_data = grad.data<tensor_t>();
auto* out_data = out.data<tensor_t>();
auto* x_data = x.data<tensor_t>();
auto* value_data = value.data<tensor_t>();
std::vector<int> num_elements;
const bool is_min_max = reduce == "amin" || reduce == "amax";
if (is_min_max) num_elements.resize(self.numel(), 0);
const int ndim = index.dims().size();
const int64_t index_size = index.numel();
{ // `cm` should be destroyed once the computation is done, no reuse
CoordinateManager<true> cm(
index.dims(), self.strides(), ndim, dim, &grad.strides());
for (int64_t i = 0; i < index_size; i++) {
int64_t index = index_data[i];
cm.CalculateOffset(index);
int64_t replace_index_self = cm.offset1;
int64_t replace_index_grad = cm.offset2;
if (is_min_max &&
out_data[replace_index_self] == value_data[replace_index_grad]) {
num_elements[replace_index_self] += 1;
} else if (!is_min_max) {
if (value_data[replace_index_grad] != static_cast<tensor_t>(0)) {
grad_data[replace_index_grad] =
self_data[replace_index_self] *
(out_data[replace_index_self] / value_data[replace_index_grad]);
} else {
grad_data[replace_index_grad] = static_cast<tensor_t>(0);
}
}
}
}
if (is_min_max) {
CoordinateManager<true> cm(
index.dims(), self.strides(), ndim, dim, &grad.strides());
for (int64_t i = 0; i < index_size; i++) {
int64_t index = index_data[i];
cm.CalculateOffset(index);
int64_t replace_index_self = cm.offset1;
int64_t replace_index_grad = cm.offset2;
if (out_data[replace_index_self] == value_data[replace_index_grad]) {
if (out_data[replace_index_self] == x_data[replace_index_self])
grad_data[replace_index_grad] =
self_data[replace_index_self] /
static_cast<tensor_t>(num_elements[replace_index_self] + 1);
else
grad_data[replace_index_grad] =
self_data[replace_index_self] /
static_cast<tensor_t>(num_elements[replace_index_self]);
}
}
}
}
Instantiate_Template_Function(cpu_gather_kernel) // NOLINT
Instantiate_Template_Function(cpu_scatter_assign_kernel) // NOLINT
Instantiate_Template_Function(cpu_scatter_add_kernel) // NOLINT
Instantiate_Template_Function(cpu_scatter_mul_kernel) // NOLINT
Instantiate_Template_Function(cpu_scatter_mean_kernel) // NOLINT
Instantiate_Template_Function(cpu_scatter_max_kernel) // NOLINT
Instantiate_Template_Function(cpu_scatter_min_kernel) // NOLINT
Instantiate_Template_Function(cpu_scatter_input_grad_kernel) // NOLINT
Instantiate_Template_Function(cpu_scatter_value_grad_kernel) // NOLINT
Instantiate_Template_Function_With_Out(
cpu_scatter_mul_min_max_input_grad_kernel) // NOLINT
Instantiate_Template_Function(cpu_scatter_mean_input_grad_kernel) // NOLINT
Instantiate_Template_Function_With_Out(
cpu_scatter_add_mean_value_grad_kernel) // NOLINT
Instantiate_Template_Function_With_Out(
cpu_scatter_mul_min_max_value_grad_kernel) // NOLINT
} // namespace phi::funcs