/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ /*! * \file topi/reduction.h * \brief Reduction op constructors */ #ifndef TVM_TOPI_REDUCTION_H_ #define TVM_TOPI_REDUCTION_H_ #include #include #include #include #include #include #include #include #include #include #include namespace tvm { namespace topi { using namespace tvm::te; /*! \brief The operation to use for CommReduce */ using FReduce = std::function& axis, ffi::Array init, Span span)>; /*! \brief The operation to use for CommReduceIdx */ using FCommReduce = std::function( ffi::Array exprs, const ffi::Array& axis, PrimExpr* condition)>; /*! * \brief Convert a reduction axis which could be empty or have negative * elements into a real axis with valid dimension indices. * * \param ndim Number of dimensions in the target. * \param axis The axis parameter. * * \return A sorted array of valid dimension indices, with no duplicates. * If the input axis is None, the result will be an axis including all dimensions. * If any input element is negative, it will be treated as an offset from the * last dimension (same as python indexing rules). */ inline std::vector GetRealAxis(int ndim, const ffi::Optional>& axis) { std::vector real_axis; if (!axis.has_value()) { for (int i = 0; i < ndim; ++i) { real_axis.push_back(i); } } else { // Use a set so duplicates are removed and the dims are sorted for (int64_t elem : axis.value()) { int64_t val = elem; if (val < 0) { val += ndim; } TVM_FFI_ICHECK_LT(val, ndim) << " exceeds the maximum dimension " << ndim; TVM_FFI_ICHECK_GE(val, 0); real_axis.push_back(static_cast(val)); } std::sort(real_axis.begin(), real_axis.end()); real_axis.resize(std::unique(real_axis.begin(), real_axis.end()) - real_axis.begin()); } return real_axis; } /*! \brief Enumerate the axes for a reduce op */ inline ffi::Array MakeReduceAxes(const std::vector& real_axis, const Tensor& data) { ffi::Array reduce_axes; for (auto i : real_axis) { std::string name = "k" + std::to_string(i); reduce_axes.push_back(tvm::te::reduce_axis(Range(0, data->shape[i]), name)); } return reduce_axes; } /*! \brief Calculate the target shape for a reduce op */ inline ffi::Array MakeReduceTargetShape(const std::vector& real_axis, const Tensor& data, bool keepdims, bool atleast1d) { auto ndim = data->shape.size(); ffi::Array target_shape; if (keepdims) { for (size_t i = 0; i < ndim; ++i) { if (std::find(real_axis.begin(), real_axis.end(), i) != real_axis.end()) { // real_axis contains i target_shape.push_back(1); } else { target_shape.push_back(data->shape[i]); } } } else { for (size_t i = 0; i < ndim; ++i) { if (std::find(real_axis.begin(), real_axis.end(), i) == real_axis.end()) { // real_axis does not contain i target_shape.push_back(data->shape[i]); } } } if (target_shape.size() == 0 && atleast1d) { target_shape.push_back(1); } return target_shape; } /*! * \brief Create a reduction operation. * * \param data The input tensor. * \param func The reduction function eg. tvm::sum * \param target_shape The output Tensor shape. * \param reduce_axes The real axes along which the reduction is performed. * \param squeeze_axes The real axes to squeeze. Unsqueezed, reduced axes will * have shape 1 in the output tensor. * \param span The location of this reducer in the source. * * \return The result tensor. */ inline Tensor DoCommReduce(const Tensor& data, FReduce func, const ffi::Array& target_shape, const std::vector& reduce_axes, const std::vector& squeeze_axes, Span span = Span()) { auto r_axes = MakeReduceAxes(reduce_axes, data); auto compute = [&](const ffi::Array& indices) { ffi::Array eval_range; ffi::Array eval_indices; int arg_counter = 0; int red_counter = 0; for (size_t i = 0; i < data->shape.size(); ++i) { bool squeeze_i = std::find(squeeze_axes.begin(), squeeze_axes.end(), i) != squeeze_axes.end(); if (std::find(reduce_axes.begin(), reduce_axes.end(), i) != reduce_axes.end()) { // real_axis contains i eval_range.push_back(r_axes[red_counter]); eval_indices.push_back(r_axes[red_counter]->var); red_counter++; arg_counter += !squeeze_i; continue; } eval_range.push_back(indices[arg_counter]); arg_counter++; } return func(data(eval_range), r_axes, {}, span); }; return tvm::te::compute(target_shape, compute, data->op->name + "_red", kCommReduce); } /*! * \brief Create a reduction operation. * * \param data The input tensor. * \param axis The axes along which the reduction is performed. * \param func The reduction function eg. tvm::sum * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * * \return The result tensor. */ inline Tensor CommReduce(const Tensor& data, const ffi::Optional>& axis, FReduce func, bool keepdims, bool atleast1d) { auto ndim = data->shape.size(); if (ndim == 0) { auto identity = topi::identity(data, data->op->name + "_red", kCommReduce); return atleast1d ? topi::expand_dims(identity, 0, 1) : identity; } auto real_axis = GetRealAxis(static_cast(ndim), axis); auto target_shape = MakeReduceTargetShape(real_axis, data, keepdims, atleast1d); return DoCommReduce(data, func, target_shape, real_axis, keepdims ? std::vector() : real_axis); } /*! * \brief Create an index reduction operation. * * \param data The input tensor. * \param axis The axes along which the reduction is performed. * \param func The reduction function * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * * \return The result tensor. */ inline Tensor CommReduceIdx(const Tensor& data, const ffi::Optional>& axis, FCommReduce func, bool keepdims, bool atleast1d) { auto ndim = data->shape.size(); TVM_FFI_ICHECK_NE(ndim, 0) << "Cannot reduce a 0 dim Tensor"; auto real_axis = GetRealAxis(static_cast(ndim), axis); auto reduce_axes = MakeReduceAxes(real_axis, data); auto target_shape = MakeReduceTargetShape(real_axis, data, keepdims, atleast1d); auto compute = [ndim, keepdims, &real_axis, &reduce_axes, &func, &data](const ffi::Array& indices) { ffi::Array eval_range; ffi::Array eval_indices; int arg_counter = 0; int red_counter = 0; for (size_t i = 0; i < ndim; ++i) { if (std::find(real_axis.begin(), real_axis.end(), i) != real_axis.end()) { // real_axis contains i eval_range.push_back(reduce_axes[red_counter]); eval_indices.push_back(reduce_axes[red_counter]->var); red_counter++; } else { if (!keepdims) { eval_range.push_back(indices[arg_counter]); arg_counter++; } else { eval_range.push_back(indices[i]); } } } ffi::Array ravel_shape; for (auto i : real_axis) { ravel_shape.push_back(data->shape[i]); } auto idx = detail::RavelIndex(eval_indices, ravel_shape); return func({idx, data(eval_range)}, reduce_axes, nullptr); }; auto temp_idx_val = tvm::te::compute(target_shape, compute, data->op->name + "_red_temp", kCommReduceIdx); auto temp_idx = temp_idx_val[0]; auto temp_val = temp_idx_val[1]; return tvm::te::compute( target_shape, [&temp_idx](const ffi::Array& indices) { return temp_idx(indices); }, data->op->name + "_red", kCommReduceIdx); } /*! \brief A combiner function for a reduction */ using FCombine = std::function(ffi::Array lhs, ffi::Array rhs)>; /*! \brief An initializer function for a reduction */ using FIdentity = std::function(std::vector types)>; /*! * \brief Create a commutative reducer for a reduction * * \param fcombine A function to combine exprs * \param fidentity A function to initialize elements * \param name The name of the operation * * \return A reducer function which creates a reduce expression over an axis. */ inline FCommReduce MakeCommReducer(FCombine fcombine, FIdentity fidentity, std::string name = "reduce") { return [fcombine, fidentity, name](ffi::Array exprs, const ffi::Array& axis, PrimExpr* condition) { ffi::Array lhs, rhs; ffi::Array callback_lhs, callback_rhs; std::vector dtypes; for (size_t i = 0; i < exprs.size(); ++i) { PrimType dtype = exprs[i].ty(); dtypes.push_back(dtype); PrimVar lhs_var(name + "_lhs_" + std::to_string(i), dtype); PrimVar rhs_var(name + "_rhs_" + std::to_string(i), dtype); lhs.push_back(lhs_var); rhs.push_back(rhs_var); callback_lhs.push_back(lhs_var); callback_rhs.push_back(rhs_var); } auto result = fcombine(callback_lhs, callback_rhs); auto id_elem = fidentity(dtypes); auto cond = condition != nullptr ? *condition : IntImm::Bool(true); auto combiner = tvm::tirx::CommReducer(lhs, rhs, result, id_elem); ffi::Array outputs; for (size_t i = 0; i < exprs.size(); ++i) { outputs.push_back(tvm::tirx::Reduce(combiner, exprs, axis, cond, static_cast(i), {})); } return outputs; }; } /*! \brief Wrap tvm::min to ensure we get the correct overload */ inline PrimExpr MinOp(PrimExpr source, ffi::Array axis, ffi::Array init = {}, Span span = Span()) { return tvm::min(source, axis, init, span); } /*! \brief Wrap tvm::max to ensure we get the correct overload */ inline PrimExpr MaxOp(PrimExpr source, ffi::Array axis, ffi::Array init = {}, Span span = Span()) { return tvm::max(source, axis, init, span); // NOLINT(*) } /*! \brief Wrap tvm::prod to ensure we get the correct overload */ inline PrimExpr ProdOp(PrimExpr source, ffi::Array axis, ffi::Array init = {}, Span span = Span()) { return tvm::prod(source, axis, init, span); // NOLINT(*) } /*! * \brief Creates an operation that sums array elements over a given axis * * \param data The input tensor * \param axis The axis to sum over. If axis is empty, the operation will * sum over all elements of the array. * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * * \return A Tensor whose op member is the sum operation */ inline Tensor sum(const Tensor& data, const ffi::Optional>& axis, bool keepdims = false, bool atleast1d = false) { // Reduction dispatch only depends on boolean element kind; lane encoding is irrelevant here. if (data->dtype.code() == DLDataTypeCode::kDLBool) { return CommReduce(data, axis, tvm::any, keepdims, atleast1d); } else { return CommReduce(data, axis, tvm::sum, keepdims, atleast1d); } } inline Tensor collapse_sum(const Tensor& data, ffi::Array target_shape) { const auto& ishape = data->shape; const auto& oshape = target_shape; int isize = data->shape.size(); int osize = target_shape.size(); TVM_FFI_ICHECK_GE(isize, osize) << "Invalid collapse: input dimensionality smaller than output dimensionality.\ninput shape: " << data->shape << "\nvs\noutput shape: " << target_shape; std::vector reduce_axes; std::vector squeeze_axes; tvm::PrimExpr one(1); for (int i_ax = isize - 1, o_ax = osize - 1; i_ax >= 0; --i_ax) { if (o_ax >= 0 && topi::detail::EqualCheck(ishape[i_ax], oshape[o_ax])) { --o_ax; continue; } reduce_axes.push_back(i_ax); if (o_ax < 0) { // squeeze o_ax if was added during expansion squeeze_axes.push_back(i_ax); } else if (topi::detail::EqualCheck(one, oshape[o_ax])) { --o_ax; } } if (reduce_axes.size() == 0) return topi::identity(data, "tensor", kCommReduce); std::reverse(reduce_axes.begin(), reduce_axes.end()); std::reverse(squeeze_axes.begin(), squeeze_axes.end()); return DoCommReduce(data, tvm::sum, target_shape, reduce_axes, squeeze_axes); } /*! * \brief Creates an operation that computes the logical AND of elements * over a given axis * * \param data The input boolean tensor * \param axis The axes to reduce. If axis is empty, the operation will * perform logical AND over all elements of the array. * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * * \return A Tensor whose op member is the all operation */ inline Tensor all(const Tensor& data, const ffi::Optional>& axis, bool keepdims = false, bool atleast1d = false) { return CommReduce(data, axis, tvm::all, keepdims, atleast1d); } /*! * \brief Creates an operation that computes the logical OR of elements * over a given axis * * \param data The input boolean tensor * \param axis The axes to reduce. If axis is empty, the operation will * perform logical OR over all elements of the array. * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * * \return A Tensor whose op member is the all operation */ inline Tensor any(const Tensor& data, const ffi::Optional>& axis, bool keepdims = false, bool atleast1d = false) { return CommReduce(data, axis, tvm::any, keepdims, atleast1d); } /*! * \brief Creates an operation that finds the minimum of elements over * a given axis. * * \param data The input tensor * \param axis The axis to find the minimum over. If axis is empty, the * operation will find the minimum over all elements of the array. * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * * \return A Tensor whose op member is the min operation */ inline Tensor min(const Tensor& data, const ffi::Optional>& axis, bool keepdims = false, bool atleast1d = false) { return CommReduce(data, axis, MinOp, keepdims, atleast1d); } /*! * \brief Creates an operation that finds the maximum of elements over * a given axis. * * \param data The input tensor * \param axis The axis to find the maximum over. If axis is empty, the * operation will find the maximum over all elements of the array. * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * * \return A Tensor whose op member is the max operation */ inline Tensor max(const Tensor& data, const ffi::Optional>& axis, bool keepdims = false, bool atleast1d = false) { return CommReduce(data, axis, MaxOp, keepdims, atleast1d); } inline FCommReduce MakeArgminReducer(bool select_last_index = false) { // Create a Commutative Reducer with a comparison operation, and method to get the initial value. auto fcombine = [=](ffi::Array lhs, ffi::Array rhs) { ffi::Array result; // Casting to avoid operator ambiguity PrimExpr lhs_idx = static_cast(lhs[0]); PrimExpr rhs_idx = static_cast(rhs[0]); PrimExpr lhs_val = static_cast(lhs[1]); PrimExpr rhs_val = static_cast(rhs[1]); // These variables compare the actual values of the array auto is_smaller = lhs_val < rhs_val; auto is_same = lhs_val == rhs_val; // This checks if the indices are correct for the reduction. E.g. for select_last_index // it gives precedence for later indices of the same element and precedence for sooner // indices if not select_last_index; PrimExpr proper_index; if (select_last_index) { proper_index = lhs_idx > rhs_idx; } else { proper_index = lhs_idx < rhs_idx; } PrimExpr update_index = is_smaller || (is_same && proper_index); result.push_back(tvm::tirx::Select(update_index, lhs[0], rhs[0])); // idx result.push_back(tvm::tirx::Select(is_smaller, lhs[1], rhs[1])); // val return result; }; auto fidentity = [&](std::vector types) { ffi::Array result; result.push_back(tvm::tirx::MakeConst(types[0], -1)); // idx result.push_back(tvm::max_value(types[1])); // val return result; }; return MakeCommReducer(fcombine, fidentity, "argmin"); } /*! * \brief Creates an operation that finds the indices of the minimum * values over a given axis. * * \param data The input tensor * \param axis The axis along which the argmin is performed. If axis is empty, * the operation will find the minimum index over all elements of the array. * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * \param select_last_index Whether to select the last index if the minimum element * appears multiple times, else select the first index. * * \return A Tensor whose op member is the argmin operation */ inline Tensor argmin(const Tensor& data, const ffi::Optional>& axis, bool keepdims = false, bool atleast1d = false, bool select_last_index = false) { auto reducer = MakeArgminReducer(select_last_index); return CommReduceIdx(data, axis, reducer, keepdims, atleast1d); } inline FCommReduce MakeArgmaxReducer(bool select_last_index = false) { // Create a Commutative Reducer with a comparison operation, and method to get the initial value. auto fcombine = [=](ffi::Array lhs, ffi::Array rhs) { ffi::Array result; // Casting to avoid operator ambiguity PrimExpr lhs_idx = static_cast(lhs[0]); PrimExpr rhs_idx = static_cast(rhs[0]); PrimExpr lhs_val = static_cast(lhs[1]); PrimExpr rhs_val = static_cast(rhs[1]); // These variables compare the actual values of the array auto is_bigger = lhs_val > rhs_val; auto is_same = lhs_val == rhs_val; // This checks if the indices are correct for the reduction. E.g. for select_last_index // it gives precedence for later indices of the same element and precedence for sooner // indices if not select_last_index; PrimExpr proper_index; if (select_last_index) { proper_index = lhs_idx > rhs_idx; } else { proper_index = lhs_idx < rhs_idx; } PrimExpr update_index = is_bigger || (is_same && proper_index); result.push_back(tvm::tirx::Select(update_index, lhs[0], rhs[0])); // idx result.push_back(tvm::tirx::Select(is_bigger, lhs[1], rhs[1])); // val return result; }; auto fidentity = [&](std::vector types) { ffi::Array result; result.push_back(tvm::tirx::MakeConst(types[0], -1)); // idx result.push_back(tvm::min_value(types[1])); // val return result; }; return MakeCommReducer(fcombine, fidentity, "argmax"); } /*! * \brief Creates an operation that finds the indices of the maximum * values over a given axis. * * \param data The input tensor * \param axis The axis along which the argmax is performed. If axis is empty, * the operation will find the maximum index over all elements of the array. * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * \param select_last_index Whether to select the last index if the maximum element * appears multiple times, else select the first index. * \return A Tensor whose op member is the argmax operation */ inline Tensor argmax(const Tensor& data, const ffi::Optional>& axis, bool keepdims = false, bool atleast1d = false, bool select_last_index = false) { auto reducer = MakeArgmaxReducer(select_last_index); return CommReduceIdx(data, axis, reducer, keepdims, atleast1d); } /*! * \brief Creates product operation over given axis. * * \param data The input tensor * \param axis The axis to do product over. If axis is empty, the * operation will do the product over all elements of the array. * \param keepdims If this is set to true, the axes which are reduced are * left in the result as dimensions with size one. This enables the result * to broadcast correctly against the input array. * \param atleast1d Whether the output need to be atleast1d. * * \return A Tensor whose op member is the prod operation */ inline Tensor prod(const Tensor& data, const ffi::Optional>& axis, bool keepdims = false, bool atleast1d = false) { return CommReduce(data, axis, ProdOp, keepdims, atleast1d); } /*! * \brief Create communitive reducer summing over tuples */ inline FCommReduce MakeTupleSumReducer() { auto fcombine = [](ffi::Array lhs, ffi::Array rhs) { ffi::Array result; TVM_FFI_ICHECK_EQ(lhs.size(), rhs.size()); result.reserve(lhs.size()); for (size_t i = 0; i < lhs.size(); ++i) { result.push_back(lhs[i] + rhs[i]); } return result; }; auto fidentity = [](std::vector types) { ffi::Array result; for (size_t i = 0; i < types.size(); ++i) { result.push_back(tvm::tirx::MakeConst(types[i], 0)); } return result; }; return MakeCommReducer(fcombine, fidentity, "tuple_sum"); } } // namespace topi } // namespace tvm #endif // TVM_TOPI_REDUCTION_H_