382 lines
14 KiB
C++
382 lines
14 KiB
C++
/* Copyright 2018 The TensorFlow 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 <cstdint>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "absl/log/check.h"
|
|
#include "absl/status/status.h"
|
|
#include "absl/strings/str_cat.h"
|
|
#include "tensorflow/compiler/tf2xla/kernels/gather_op_helpers.h"
|
|
#include "tensorflow/compiler/tf2xla/mlir_xla_op_kernel.h"
|
|
#include "tensorflow/compiler/tf2xla/shape_util.h"
|
|
#include "tensorflow/compiler/tf2xla/type_util.h"
|
|
#include "tensorflow/compiler/tf2xla/xla_context.h"
|
|
#include "tensorflow/compiler/tf2xla/xla_helpers.h"
|
|
#include "tensorflow/compiler/tf2xla/xla_op_kernel.h"
|
|
#include "tensorflow/compiler/tf2xla/xla_op_registry.h"
|
|
#include "xla/hlo/builder/lib/slicing.h"
|
|
#include "xla/hlo/builder/xla_builder.h"
|
|
#include "xla/status_macros.h"
|
|
#include "xla/xla_data.pb.h"
|
|
#include "tensorflow/core/framework/kernel_def_builder.h"
|
|
#include "tensorflow/core/framework/op_kernel.h"
|
|
#include "tensorflow/core/framework/types.pb.h"
|
|
#include "tensorflow/core/lib/core/errors.h"
|
|
|
|
namespace tensorflow {
|
|
|
|
absl::Status XlaGather(const xla::XlaOp& input, const TensorShape& input_shape,
|
|
const xla::XlaOp& indices,
|
|
const TensorShape& indices_shape, int64_t axis,
|
|
bool indices_are_nd, DataType dtype, DataType index_type,
|
|
xla::XlaBuilder* builder, xla::XlaOp* gather_output) {
|
|
// There is no deep reason why we need this precondition, but this is the only
|
|
// combination that is used and tested today.
|
|
CHECK(!indices_are_nd || axis == 0);
|
|
|
|
// num_index_dims is the number of components in each index in the indices
|
|
// tensor.
|
|
//
|
|
// num_indices is the total number of (n dimensional or scalar) indices in the
|
|
// indices tensor.
|
|
//
|
|
// If the indices are N-dimensional, then the minor dimension of indices
|
|
// should be of size N and correspond to the N indices.
|
|
int64_t num_index_dims;
|
|
int64_t num_indices = 1;
|
|
if (indices_are_nd) {
|
|
CHECK_GE(indices_shape.dims(), 1);
|
|
num_index_dims = indices_shape.dim_size(indices_shape.dims() - 1);
|
|
for (int64_t i = 0, e = indices_shape.dims() - 1; i < e; i++) {
|
|
num_indices *= indices_shape.dim_size(i);
|
|
}
|
|
} else {
|
|
num_index_dims = 1;
|
|
for (int64_t i = 0, e = indices_shape.dims(); i < e; i++) {
|
|
num_indices *= indices_shape.dim_size(i);
|
|
}
|
|
}
|
|
|
|
// Degenerate case: empty indices.
|
|
if (num_indices == 0) {
|
|
TensorShape input_shape_pre_axis{input_shape};
|
|
input_shape_pre_axis.RemoveDimRange(axis, input_shape.dims());
|
|
TensorShape input_shape_post_axis{input_shape};
|
|
input_shape_post_axis.RemoveDimRange(0, axis + num_index_dims);
|
|
|
|
TensorShape indices_shape_no_index_vectors{indices_shape};
|
|
if (indices_are_nd) {
|
|
indices_shape_no_index_vectors.RemoveLastDims(1);
|
|
}
|
|
|
|
TensorShape out_shape;
|
|
out_shape.AppendShape(input_shape_pre_axis);
|
|
out_shape.AppendShape(indices_shape_no_index_vectors);
|
|
out_shape.AppendShape(input_shape_post_axis);
|
|
|
|
*gather_output =
|
|
xla::Broadcast(XlaHelpers::Zero(builder, dtype), out_shape.dim_sizes());
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
for (int64_t i = 0; i < num_index_dims; ++i) {
|
|
if (input_shape.dim_size(axis + i) == 0) {
|
|
// Gather dimension of size zero in tensor results in constant 0.
|
|
// This is done to match the legacy behavior of the MLIR legalization and
|
|
// avoid breaking existing models.
|
|
auto slice_sizes = input_shape.dim_sizes();
|
|
slice_sizes.erase(slice_sizes.begin() + axis);
|
|
*gather_output =
|
|
xla::Broadcast(XlaHelpers::Zero(builder, dtype), slice_sizes);
|
|
return absl::OkStatus();
|
|
}
|
|
}
|
|
|
|
// Example of a 1-D gather with axis=1, pulling two [3,1] tensors out of a
|
|
// tensor of shape [3,3].
|
|
//
|
|
// operand = s32[3,3] parameter(0)
|
|
// indices = s32[2] parameter(1)
|
|
// gather = s32[3,2] gather(operand, indices),
|
|
// offset_dims={0},
|
|
// collapsed_slice_dims={1},
|
|
// start_index_map={1},
|
|
// index_vector_dim=1,
|
|
// slice_sizes={3, 1}
|
|
//
|
|
//
|
|
// Example of an N-D gather pulling out slices of shape [1,1,2] out of a
|
|
// tensor of shape [3,3,2].
|
|
//
|
|
// operand = s32[3,3,2] parameter(0)
|
|
// indices = s32[2,2] parameter(1)
|
|
// gather = s32[2,2] gather(operand, indices),
|
|
// offset_dims={1},
|
|
// collapsed_slice_dims={0,1},
|
|
// start_index_map={0,1},
|
|
// index_vector_dim=0,
|
|
// slice_sizes={1,1,2}
|
|
|
|
xla::GatherDimensionNumbers dim_numbers;
|
|
std::vector<int64_t> slice_sizes;
|
|
slice_sizes.reserve(input_shape.dims());
|
|
for (int64_t i = 0; i < input_shape.dims(); i++) {
|
|
int64_t window_bound;
|
|
if (axis <= i && i < (axis + num_index_dims)) {
|
|
dim_numbers.add_collapsed_slice_dims(i);
|
|
window_bound = 1;
|
|
} else {
|
|
window_bound = input_shape.dim_size(i);
|
|
}
|
|
|
|
slice_sizes.push_back(window_bound);
|
|
|
|
if (i < axis) {
|
|
dim_numbers.add_offset_dims(i);
|
|
} else if (i >= (axis + num_index_dims)) {
|
|
int64_t indices_rank =
|
|
indices_are_nd ? (indices_shape.dims() - 1) : indices_shape.dims();
|
|
dim_numbers.add_offset_dims(i + indices_rank - num_index_dims);
|
|
}
|
|
}
|
|
|
|
dim_numbers.set_index_vector_dim(indices_are_nd ? (indices_shape.dims() - 1)
|
|
: indices_shape.dims());
|
|
for (int64_t i = axis; i < axis + num_index_dims; i++) {
|
|
dim_numbers.add_start_index_map(i);
|
|
}
|
|
|
|
*gather_output = xla::Gather(input, indices, dim_numbers, slice_sizes);
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status XlaGatherWithBatchDimsOpImpl(XlaOpKernelContext* context,
|
|
const xla::XlaOp input,
|
|
const TensorShape& input_shape,
|
|
int batch_dims,
|
|
xla::XlaOp* gather_output) {
|
|
auto indices = context->Input(1);
|
|
auto indices_shape = context->InputShape(1);
|
|
|
|
std::optional<int64_t> axis;
|
|
if (context->num_inputs() == 3) {
|
|
const TensorShape axis_shape = context->InputShape(2);
|
|
if (!TensorShapeUtils::IsScalar(axis_shape)) {
|
|
return absl::InvalidArgumentError("axis must be scalar");
|
|
}
|
|
DataType axis_type = context->input_type(2);
|
|
if (axis_type != DT_INT32 && axis_type != DT_INT64) {
|
|
return absl::InvalidArgumentError("axis must be int32 or int64");
|
|
}
|
|
|
|
int64_t axis_input;
|
|
TF_RETURN_IF_ERROR(context->ConstantInputAsIntScalar(2, &axis_input));
|
|
|
|
const auto params_dims = input_shape.dims();
|
|
if (-params_dims > axis_input || axis_input >= params_dims) {
|
|
// Check that params has rank of at least axis + 1.
|
|
const auto min_params_rank =
|
|
axis_input < 0 ? -axis_input : axis_input + 1;
|
|
return absl::InvalidArgumentError(
|
|
absl::StrCat("Shape must be at least rank ", min_params_rank,
|
|
" but is rank ", params_dims));
|
|
}
|
|
if (axis_input < 0) {
|
|
axis_input += params_dims;
|
|
}
|
|
axis = axis_input;
|
|
}
|
|
|
|
if (batch_dims != 0) {
|
|
if (batch_dims < 0) {
|
|
batch_dims = indices_shape.dims() + batch_dims;
|
|
}
|
|
|
|
axis = axis.value_or(batch_dims);
|
|
|
|
if (batch_dims < -indices_shape.dims() ||
|
|
batch_dims > indices_shape.dims()) {
|
|
return absl::InvalidArgumentError(absl::StrCat(
|
|
"Expected batch_dims in the range [", -indices_shape.dims(), ", ",
|
|
indices_shape.dims(), "], but got ", batch_dims));
|
|
}
|
|
|
|
if (batch_dims >= input_shape.dims()) {
|
|
return absl::InvalidArgumentError(absl::StrCat(
|
|
"batch_dims (", batch_dims, ") must be less than rank(input) (",
|
|
input_shape.dims(), ")."));
|
|
}
|
|
|
|
if (*axis < batch_dims) {
|
|
return absl::InvalidArgumentError(absl::StrCat(
|
|
"batch_dims (", batch_dims, ") must be less than or equal to ",
|
|
"axis (", *axis, ")."));
|
|
}
|
|
}
|
|
|
|
axis = axis.value_or(0);
|
|
DataType index_type = context->input_type(1);
|
|
if (index_type != DT_INT16 && index_type != DT_INT32 &&
|
|
index_type != DT_INT64) {
|
|
return absl::InvalidArgumentError("indices must be int16, int32, or int64");
|
|
}
|
|
|
|
xla::XlaOp gather;
|
|
if (batch_dims > 0) {
|
|
*gather_output = xla::TorchIndexSelect(input, indices, *axis, batch_dims);
|
|
} else {
|
|
// XlaGather() manages degenerate cases, like empty-indices, which are
|
|
// error conditions and caught above if batch_dims is not 0.
|
|
TF_RETURN_IF_ERROR(
|
|
XlaGather(input, input_shape, indices, indices_shape, *axis,
|
|
/*indices_are_nd=*/false, context->expected_output_dtype(0),
|
|
index_type, context->builder(), gather_output));
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
class GatherOp : public XlaOpKernel {
|
|
public:
|
|
explicit GatherOp(OpKernelConstruction* context) : XlaOpKernel(context) {
|
|
// Set batch_dims_ to 0 if the attribute does not exist.
|
|
if (context->HasAttr("batch_dims")) {
|
|
OP_REQUIRES_OK(context, context->GetAttr("batch_dims", &batch_dims_));
|
|
} else {
|
|
batch_dims_ = 0;
|
|
}
|
|
}
|
|
|
|
void Compile(XlaOpKernelContext* context) override {
|
|
auto input = context->Input(0);
|
|
auto input_shape = context->InputShape(0);
|
|
|
|
xla::XlaOp gather;
|
|
OP_REQUIRES_OK(context,
|
|
XlaGatherWithBatchDimsOpImpl(context, input, input_shape,
|
|
batch_dims_, &gather));
|
|
context->SetOutput(0, gather);
|
|
}
|
|
|
|
private:
|
|
GatherOp(const GatherOp&) = delete;
|
|
void operator=(const GatherOp&) = delete;
|
|
|
|
// The number of batch dimensions, as passed in the batch_dims attribute.
|
|
// It must be less than or equal to rank(indices).
|
|
int32_t batch_dims_ = 0;
|
|
};
|
|
|
|
REGISTER_XLA_OP(Name("Gather"), MlirXlaOpKernel);
|
|
REGISTER_XLA_OP(Name("GatherV2").CompileTimeConstantInput("axis"), GatherOp);
|
|
|
|
class GatherNdOp : public XlaOpKernel {
|
|
public:
|
|
explicit GatherNdOp(OpKernelConstruction* context) : XlaOpKernel(context) {
|
|
if (context->HasAttr("bad_indices_policy")) {
|
|
OP_REQUIRES_OK(context, context->GetAttr("bad_indices_policy",
|
|
&bad_indices_policy_));
|
|
}
|
|
}
|
|
|
|
void Compile(XlaOpKernelContext* context) override {
|
|
DataType params_type = context->input_type(0);
|
|
DataType indices_type = context->input_type(1);
|
|
|
|
TensorShape params_shape = context->InputShape(0);
|
|
TensorShape indices_shape = context->InputShape(1);
|
|
OP_REQUIRES(context, TensorShapeUtils::IsVectorOrHigher(params_shape),
|
|
absl::InvalidArgumentError("params must be at least a vector"));
|
|
OP_REQUIRES(
|
|
context, TensorShapeUtils::IsVectorOrHigher(indices_shape),
|
|
absl::InvalidArgumentError("indices must be at least a vector"));
|
|
const int64_t num_index_dims =
|
|
indices_shape.dim_size(indices_shape.dims() - 1);
|
|
OP_REQUIRES(
|
|
context, num_index_dims <= params_shape.dims(),
|
|
absl::InvalidArgumentError(absl::StrCat(
|
|
"index innermost dimension length must be <= params rank; saw: ",
|
|
indices_shape.dim_size(indices_shape.dims() - 1), " vs. ",
|
|
params_shape.dims())));
|
|
|
|
xla::XlaBuilder* builder = context->builder();
|
|
auto params = context->Input(0);
|
|
auto indices = context->Input(1);
|
|
xla::XlaOp gather;
|
|
OP_REQUIRES_OK(context, XlaGather(params, params_shape, indices,
|
|
indices_shape, /*axis=*/0,
|
|
/*indices_are_nd=*/true, params_type,
|
|
indices_type, builder, &gather));
|
|
// By default, XLA clips OOB indices, while "IGNORE" policy demands to fill
|
|
// 0s to the output. The following code implements the "IGNORE" policy by
|
|
// masking the gather result with the valid indices mask.
|
|
if (bad_indices_policy_ == "IGNORE") {
|
|
xla::XlaOp valid_mask;
|
|
for (int i = 0; i < num_index_dims; ++i) {
|
|
xla::XlaOp i_limit = XlaHelpers::IntegerLiteral(
|
|
builder, indices_type, params_shape.dim_size(i));
|
|
xla::XlaOp i_zero = XlaHelpers::Zero(builder, indices_type);
|
|
xla::XlaOp indices_i =
|
|
xla::SliceInDim(indices, i, i + 1, 1, indices_shape.dims() - 1);
|
|
|
|
xla::XlaOp indices_i_good =
|
|
xla::And(xla::Ge(indices_i, i_zero), xla::Lt(indices_i, i_limit));
|
|
if (i == 0) {
|
|
valid_mask = indices_i_good;
|
|
} else {
|
|
valid_mask = xla::And(valid_mask, indices_i_good);
|
|
}
|
|
}
|
|
auto gather_shape_status = builder->GetShape(gather);
|
|
OP_REQUIRES_OK(context, gather_shape_status.status());
|
|
auto gather_shape = gather_shape_status.value();
|
|
|
|
// The last dim of indices tensor is the index vector dimension, which is
|
|
// omitted from the gather tensor.
|
|
auto valid_mask_dims = indices_shape.dim_sizes();
|
|
valid_mask_dims.pop_back();
|
|
valid_mask = xla::Reshape(valid_mask, valid_mask_dims);
|
|
if (indices_shape.dims() != gather_shape.dimensions().size()) {
|
|
OP_REQUIRES(
|
|
context,
|
|
gather_shape.dimensions().size() == indices_shape.dims() - 1,
|
|
absl::InvalidArgumentError(
|
|
"Indices rank must be equal to output rank (with channel "
|
|
"dimension) or 1 less (w/o channel dimension)"));
|
|
} else {
|
|
std::vector<int64_t> broadcast_dims(valid_mask_dims.size(), 1);
|
|
for (int i = 0; i < broadcast_dims.size(); ++i) {
|
|
broadcast_dims[i] = i;
|
|
}
|
|
valid_mask = xla::BroadcastInDim(valid_mask, gather_shape.dimensions(),
|
|
broadcast_dims);
|
|
}
|
|
|
|
gather =
|
|
xla::Select(valid_mask, gather,
|
|
xla::Broadcast(XlaHelpers::Zero(builder, params_type),
|
|
gather_shape.dimensions()));
|
|
}
|
|
context->SetOutput(0, gather);
|
|
}
|
|
|
|
std::string bad_indices_policy_;
|
|
};
|
|
|
|
REGISTER_XLA_OP(Name("GatherNd"), GatherNdOp);
|
|
|
|
} // namespace tensorflow
|