Files
clang-p2996/mlir/lib/Dialect/Tosa/Transforms/TosaFolders.cpp
Tina Jung d84d418e2a [mlir][tosa] Constant folding for reciprocal
Add constant fold for tosa.reciprocal, which can be applied if the input is a dense constant tensor. The reciprocal is computed for every element and the result is a tensor with the same dimensions as the input tensor.

As the input tensor might require a lot of memory and the folding might double the required memory, a heuristic decides when to actually apply the folding. Currently, the operation will be replaced only if the input constant is a splat (i.e. requires little memory) or has in single user (similar to the already existing fold for constant transposes). This keeps the additionally required space low.

Differential Revision: https://reviews.llvm.org/D150578
2023-07-05 11:38:46 +02:00

303 lines
11 KiB
C++

//===- TosaFolders.cpp ----------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Fold TOSA operations
//
//===----------------------------------------------------------------------===//
#include <functional>
#include "mlir/Dialect/Tosa/IR/TosaOps.h"
#include "mlir/Dialect/Tosa/Transforms/Passes.h"
#include "mlir/Dialect/Utils/IndexingUtils.h"
#include "mlir/IR/BuiltinAttributes.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/Matchers.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Support/LogicalResult.h"
#include "llvm/ADT/APFloat.h"
#include "llvm/ADT/FloatingPointMode.h"
#include "llvm/ADT/SmallVector.h"
using namespace mlir;
using namespace mlir::tosa;
namespace {
/// Rounding mode to be used on floating point operations that require rounding.
static constexpr llvm::RoundingMode tosaRoundingMode =
llvm::APFloat::rmNearestTiesToEven;
/// Apply the given transformation \p toApply to every element of the tensor to
/// be transformed \p toTransform.
///
/// Elements of \p toTransform are extracted as \p SrcValueType.
///
/// \returns A tensor with the same size as \p toTransform, containing
/// \p TargetValueType values of type \p TargetType.
template <class SrcValType, class TargetValType, class TargetType>
DenseElementsAttr applyElementWise(
const DenseElementsAttr &toTransform,
const std::function<TargetValType(const SrcValType &, TargetType)> &toApply,
TargetType targetType) {
SmallVector<TargetValType> transformedValues;
// We already know the amount of values we will insert, reserve space for
// all of them to avoid dynamic resizing
transformedValues.reserve(toTransform.getNumElements());
for (auto val : toTransform.getValues<SrcValType>()) {
auto transformedVal = toApply(val, targetType);
transformedValues.push_back(transformedVal);
}
// Make sure that the output tensor has the expected output type
auto inShape = toTransform.getType();
auto outTy = inShape.cloneWith({}, targetType);
return DenseElementsAttr::get(outTy, transformedValues);
}
template DenseElementsAttr applyElementWise<APFloat, APFloat, FloatType>(
const DenseElementsAttr &toTransform,
const std::function<APFloat(const APFloat &, FloatType)> &toApply,
FloatType targetType);
/// Function that checks if the type contained in \p toCheck is float.
LogicalResult notifyIfNotFloat(TypedValue<TensorType> toCheck, TosaOp location,
PatternRewriter &rewriter) {
if (isa<FloatType>(toCheck.getType().getElementType())) {
return success();
}
return rewriter.notifyMatchFailure(location,
"Unexpected input tensor type: the "
"TOSA spec only allows floats");
}
/// Function that checks if \p toCheck is a dense TOSA constant tensor.
LogicalResult notifyIfNoTosaDenseConstantTensor(TypedValue<TensorType> toCheck,
TosaOp location,
PatternRewriter &rewriter) {
// Check whether the tensor is constant and dense
// TODO We currently ensure the tensor is dense by using the correct type for
// the bind_value, however we do not actually need this value. It would be
// nicer to only have a check here.
DenseElementsAttr tmp;
if (!matchPattern(toCheck, m_Constant(&tmp))) {
return rewriter.notifyMatchFailure(location,
"Non-const or non-dense input tensor");
}
// Make sure it actually is a TOSA constant (the match allows for other
// constants as well)
if (isa<ConstOp>(toCheck.getDefiningOp())) {
return success();
}
return rewriter.notifyMatchFailure(location,
"The reciprocal can only be folded if "
"it operates on a TOSA constant");
}
/// Function that checks if \p toCheck is a dense TOSA constant float tensor.
LogicalResult notifyIfNotConstantFloatTosaTensor(TypedValue<TensorType> toCheck,
TosaOp location,
PatternRewriter &rewriter) {
auto floatCheck = notifyIfNotFloat(toCheck, location, rewriter);
if (failed(floatCheck)) {
return floatCheck;
}
return notifyIfNoTosaDenseConstantTensor(toCheck, location, rewriter);
}
/// Heuristic to decide when to replace a unary operation on a constant with the
/// folded value.
/// Folding operations on constants can lead to an increased memory usage
/// whenever the input cannot be replaced but a new constant is inserted. Hence,
/// this will currently only suggest folding when the memory impact is
/// negligible.
/// Takes the \p unaryOp and the constant input \p values.
/// \returns Whether folding should be applied.
bool constantUnaryOpShouldBeFolded(TosaOp unaryOp, DenseElementsAttr values) {
assert(unaryOp->getNumOperands() == 1);
auto inputOp = unaryOp->getOperand(0);
// If the input is a splat, we don't care for the number of users
if (isa<SplatElementsAttr>(values)) {
return true;
}
// If this is the only use of the tensor it should be replaced as no
// additional memory is required
return inputOp.hasOneUse();
}
template <typename BaseType>
DenseElementsAttr transposeType(ElementsAttr attr, ShapedType inputType,
ShapedType outputType,
llvm::ArrayRef<int64_t> permValues) {
if (inputType.getNumElements() == 0)
return DenseElementsAttr::get(outputType, llvm::ArrayRef<BaseType>{});
auto attrValues = attr.getValues<BaseType>();
auto inputShape = inputType.getShape();
// The inverted permutation map and strides of the output are used to compute
// the contribution of a given dimension to the destination linear index in
// an order-independent way.
auto outputStrides = computeStrides(outputType.getShape());
auto invertedPermValues = invertPermutationVector(permValues);
auto initialValue = *std::begin(attrValues);
SmallVector<BaseType> outputValues(inputType.getNumElements(), initialValue);
for (const auto &it : llvm::enumerate(attrValues)) {
auto srcLinearIndex = it.index();
uint64_t dstLinearIndex = 0;
for (int64_t dim = inputShape.size() - 1; dim >= 0; --dim) {
// Compute the index into the current dimension of the source vector.
auto sourceIndexForDim = srcLinearIndex % inputShape[dim];
srcLinearIndex /= inputShape[dim];
// Add the contribution of the current dimension to the output using the
// permutation map.
dstLinearIndex +=
outputStrides[invertedPermValues[dim]] * sourceIndexForDim;
}
outputValues[dstLinearIndex] = it.value();
}
return DenseElementsAttr::get(outputType,
llvm::ArrayRef<BaseType>(outputValues));
}
// A type specialized transposition of an ElementsAttr.
// This implementation tries to operate on the underlying data in its raw
// representation when possible to avoid allocating a large number of Attribute
// objects.
DenseElementsAttr transpose(ElementsAttr attr, ShapedType inputType,
ShapedType outputType,
llvm::ArrayRef<int64_t> permValues) {
auto baseType = inputType.getElementType();
// Handle possible integer types
if (auto intType = dyn_cast<IntegerType>(baseType)) {
switch (intType.getWidth()) {
case 1:
return transposeType<bool>(attr, inputType, outputType, permValues);
case 8:
return transposeType<int8_t>(attr, inputType, outputType, permValues);
case 16:
return transposeType<int16_t>(attr, inputType, outputType, permValues);
case 32:
return transposeType<int32_t>(attr, inputType, outputType, permValues);
case 64:
return transposeType<int64_t>(attr, inputType, outputType, permValues);
default:
return transposeType<APInt>(attr, inputType, outputType, permValues);
}
}
// Handle possible float types
if (baseType.isF32()) {
return transposeType<float>(attr, inputType, outputType, permValues);
}
return transposeType<APFloat>(attr, inputType, outputType, permValues);
}
struct TosaFoldConstantTranspose : public OpRewritePattern<tosa::TransposeOp> {
using OpRewritePattern::OpRewritePattern;
LogicalResult matchAndRewrite(tosa::TransposeOp op,
PatternRewriter &rewriter) const override {
auto outputType = cast<ShapedType>(op.getType());
// TOSA supports quantized types.
if (!outputType.getElementType().isIntOrIndexOrFloat())
return failure();
ElementsAttr inputValues;
if (!matchPattern(op.getInput1(), m_Constant(&inputValues)))
return failure();
// Make sure the input is a constant that has a single user.
if (!llvm::hasSingleElement(op.getInput1().getDefiningOp()->getUsers()))
return failure();
DenseIntElementsAttr permAttr;
if (!matchPattern(op.getPerms(), m_Constant(&permAttr)))
return failure();
auto permValues = llvm::to_vector<6>(llvm::map_range(
// TOSA allows both 32- and 64-bit integer tensors here.
permAttr.getValues<APInt>(),
[](const APInt &val) { return val.getSExtValue(); }));
auto inputType = cast<ShapedType>(op.getInput1().getType());
auto resultAttr = transpose(inputValues, inputType, outputType, permValues);
rewriter.replaceOpWithNewOp<tosa::ConstOp>(op, outputType, resultAttr);
return success();
}
};
struct TosaFoldConstantReciprocal : public OpRewritePattern<ReciprocalOp> {
using OpRewritePattern::OpRewritePattern;
static APFloat computeReciprocal(const APFloat &floatVal, FloatType floatTy) {
auto recipAttr = FloatAttr::get(floatTy, 1.0);
APFloat recip = recipAttr.getValue();
recip.divide(floatVal, tosaRoundingMode);
return recip;
}
LogicalResult matchAndRewrite(ReciprocalOp recip,
PatternRewriter &rewriter) const override {
auto inputTensor = recip.getInput1();
// Check that we can apply folding
auto preCondCheck =
notifyIfNotConstantFloatTosaTensor(inputTensor, recip, rewriter);
if (failed(preCondCheck)) {
return preCondCheck;
}
// Extract the tensor values
DenseElementsAttr inputValues;
matchPattern(inputTensor, m_Constant(&inputValues));
// Check whether this should be folded.
if (!constantUnaryOpShouldBeFolded(recip, inputValues)) {
return rewriter.notifyMatchFailure(
recip, "Currently, reciprocals will only be folded if the input "
"tensor has a single user");
}
// Create a new tensor with the updated values
auto newTensor = applyElementWise<APFloat, APFloat, FloatType>(
inputValues, &computeReciprocal,
cast<FloatType>(inputValues.getElementType()));
// Replace the use of the reciprocal with the transformed tensor
rewriter.replaceOpWithNewOp<ConstOp>(recip, newTensor.getType(), newTensor);
return success();
}
};
} // namespace
void mlir::tosa::populateTosaFoldConstantTransposePatterns(
MLIRContext *ctx, RewritePatternSet &patterns) {
patterns.add<TosaFoldConstantTranspose>(ctx);
}
void mlir::tosa::populateTosaFoldConstantReciprocalPatterns(
MLIRContext *ctx, RewritePatternSet &patterns) {
patterns.add<TosaFoldConstantReciprocal>(ctx);
}