blob: 9964eaeb2f6dee1cc0da45146a3b80e9bfda261c [file] [log] [blame]
// Copyright 2020 Google LLC
//
// 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
//
// https://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 "iree/compiler/Dialect/Flow/Transforms/DispatchConfig.h"
#include "iree/compiler/Dialect/Flow/IR/FlowOps.h"
#include "iree/compiler/Dialect/Flow/Utils/DispatchUtils.h"
#include "iree/compiler/Dialect/Shape/IR/ShapeOps.h"
#include "llvm/Support/Debug.h"
#include "mlir/Dialect/StandardOps/IR/Ops.h"
#include "tensorflow/compiler/mlir/hlo/include/mlir-hlo/Dialect/mhlo/IR/hlo_ops.h"
#define DEBUG_TYPE "iree-detail"
namespace mlir {
namespace iree_compiler {
namespace IREE {
namespace Flow {
namespace {
// TODO(laurenzo): Every one of these should have better support and removed
// from this exclusion list eventually.
// Allowlist of ops that materialize to a an index-permuted copy of some kind
// if they exist standalone. Generally we try to avoid anchoring on these,
// letting them fuse into more meaningful ops as possible.
bool isIndexOp(Operation *op) {
// TODO(laurenzo): Curate this list more specifically (or have a better
// mechanism for determining).
return isa<Shape::RankedBroadcastInDimOp>(op) ||
isa<mhlo::BroadcastInDimOp>(op) || isa<mhlo::BroadcastOp>(op) ||
isa<mhlo::DynamicBroadcastInDimOp>(op) ||
isa<mhlo::DynamicReshapeOp>(op) || isa<mhlo::DynamicSliceOp>(op) ||
isa<mhlo::ReshapeOp>(op) || isa<mhlo::SliceOp>(op) ||
isa<mhlo::TransposeOp>(op);
}
} // namespace
//------------------------------------------------------------------------------
// OpDispatchPolicy
//------------------------------------------------------------------------------
bool OpDispatchPolicy::isDispatchable(Operation *op) {
if (FlowDialect::isDialectOp(op)) {
// Ignore things we've already produced as they should only relate to
// sequencer operations.
LLVM_DEBUG(llvm::dbgs() << " NOT DISPATCHABLE (Flow Dialect): "
<< op->getName() << "\n");
return false;
} else if (op->isKnownTerminator()) {
// Currently we skip all terminators as we want to leave them in the block
// to keep it valid. Future folding passes may take care of them if they are
// worth bringing into the dispatch region.
LLVM_DEBUG(llvm::dbgs() << " NOT DISPATCHABLE (Known Terminator): "
<< op->getName() << "\n");
return false;
} else if (auto callOp = dyn_cast<CallOp>(op)) {
bool dispatchable = dispatchability.isDispatchable(callOp.getCallee());
LLVM_DEBUG(llvm::dbgs()
<< " " << (dispatchable ? "" : "NOT ")
<< "DISPATCHABLE (Call): " << op->getName() << "\n");
return dispatchable;
} else if (isa<CallIndirectOp>(op)) {
// Indirect calls are not supported in dispatch code.
LLVM_DEBUG(llvm::dbgs() << " NOT DISPATCHABLE (Call Indirect): "
<< op->getName() << "\n");
return false;
} else if (isa<ConstantOp>(op)) {
// Constants are handled in the RematerializeDispatchConstants pass.
// We do that independently so that we can more easily see the use of
// constants across all dispatches instead of just on an individual basis
// as we do here.
LLVM_DEBUG(llvm::dbgs()
<< " NOT DISPATCHABLE (Constant): " << op->getName() << "\n");
return false;
} else if (op->getNumResults() &&
!op->getResult(0).getType().isa<ShapedType>()) {
// We don't put scalar manipulation into dispatch regions.
LLVM_DEBUG(llvm::dbgs()
<< " NOT DISPATCHABLE (Non Shaped): " << op->getName() << "\n");
return false;
} else if (!isOpOfKnownDialect(op)) {
// Probably a custom op.
LLVM_DEBUG(llvm::dbgs() << " NOT DISPATCHABLE (Unknown Dialect): "
<< op->getName() << "\n");
return false;
}
LLVM_DEBUG(llvm::dbgs() << " DISPATCHABLE: " << op->getName() << "\n");
return true;
}
bool OpDispatchPolicy::isIdentityMetadata(Operation *op) {
return isa<Shape::TieShapeOp>(op);
}
int OpDispatchPolicy::getAnchorBenefit(Operation *op) {
if (isUnsupportedFusionOp(op)) {
return 100;
}
if (isa<Shape::TieShapeOp>(op) || isa<Shape::MakeRankedShapeOp>(op)) {
// Cannot anchor.
return 0;
} else if (isIndexOp(op)) {
// We generally do not want to form anchors around ops that just do a copy
// (perhaps with an affine map) except as a last resort.
return 1;
} else if (isa<mhlo::SelectOp>(op)) {
// TODO(#2050): In a number of cases, this makes it less likely to split
// a DR across a compare/select boundary. Remove this once i1 is legalized
// properly.
return 15;
} else {
// Most dispatchable ops can anchor but are a fairly low benefit.
return 10;
}
}
OpDispatchPolicy::FusionType OpDispatchPolicy::fuseInput(Operation *anchorOp,
Operation *inputOp) {
if (inputOp->isKnownTerminator()) return FusionType::DISABLED;
if (isIdentityMetadata(inputOp)) {
// Shape ties must always be duplicated into the region and remain in their
// original position. This should apply to any such "metadata" ops.
return FusionType::CLONE_INTO;
}
if (isUnsupportedFusionOp(anchorOp) && isa<mhlo::ReshapeOp>(inputOp)) {
// Clones reshape op to the same region as its consumer.
return FusionType::CLONE_INTO;
}
if (isUnsupportedFusionOp(anchorOp) || isUnsupportedFusionOp(inputOp)) {
return FusionType::DISABLED;
}
// By default for operands, they are duplicated into the dispatch region.
// Typically at the initial fusion stage, there is not a sufficient cost
// model to determine whether it is more beneficial to fuse or materialize,
// so the bias is towards fusion and leaving inter-region analysis to a later
// phase.
return FusionType::CLONE_INTO;
}
OpDispatchPolicy::FusionType OpDispatchPolicy::fuseOutput(Operation *anchorOp,
Operation *outputOp) {
if (outputOp->isKnownTerminator() || outputOp->getNumResults() == 0) {
return FusionType::DISABLED;
}
if (isIdentityMetadata(outputOp)) {
return FusionType::MOVE_INTO;
}
if (isUnsupportedFusionOp(anchorOp) && isa<mhlo::ReshapeOp>(outputOp)) {
// Moves reshape op to the same region as its producer.
return FusionType::MOVE_INTO;
}
if (isUnsupportedFusionOp(anchorOp) || isUnsupportedFusionOp(outputOp)) {
return FusionType::DISABLED;
}
// Generally, it is hard to reason locally about the legality of fusing an
// output, since additional analysis may need to be done to determine
// workload compatibility (especially with dynamic shapes involved). As
// such, we do as little as possible here and instead rely on optimization
// passes to merge compatible regions.
return FusionType::DISABLED;
}
// TODO(b/144530470): replace with tablegen attributes/interfaces.
bool OpDispatchPolicy::isUnsupportedFusionOp(Operation *op) {
return isa<mhlo::ConcatenateOp, mhlo::ConvOp, mhlo::DotGeneralOp, mhlo::DotOp,
mhlo::PadOp, mhlo::ReduceOp, mhlo::ReduceWindowOp,
mhlo::TorchIndexSelectOp>(op) ||
isRootOnlyOp(op);
}
bool OpDispatchPolicy::isRootOnlyOp(Operation *op) {
return isa<mhlo::SliceOp>(op);
}
} // namespace Flow
} // namespace IREE
} // namespace iree_compiler
} // namespace mlir