Some NoMemoryEffect FIR ops have an impure implementation in the LLVM dialect which generates allocas. Some OpenMP operations have bodies that will be outlined and may be run on different threads. These OpenMP operations are not IsolatedFromAbove. Values used directly inside of the regions of these operations are implicitly shared between threads. It is important that the allocas generated to implement NoMemoryEffect FIR ops do not lead to accidental sharing between threads. This commit adds a fir codegen pass which avoids this by rematerializing these FIR operations inside of outlinable OpenMP regions. The problem is not inherent to the OpenMP dialect: every time an MLIR value is defined above an MLIR region where it is used, the use is semantically a copy of the value into that region. Making this copy explicit for all uses would not be practical. What this pass is doing is to copy (by rematerializing) only these values where that is relevant. Only operations implementing OutlinableOpenMPOpInterface are handled in this pass because these were the operations which already had special handling for alloca placement in flang codegen. The pass is designed to be easy to extent to other operations containing region boundaries which require this re-materialization. Similarly, the pass should be easy to extend to other operations which should be rematerialized at these region boundaries. Some fir.embox/fir.rebox operations are not speculatable; these are not susceptible to the original bug because CSE will not hoist them outside of the outlinable regions. I chose to support these anyway for completeness and because of the mlir copying semantics described above. It is safe to speculate these operations here because the rematerialized operations are rematerialized immediately before where the values would be used. Furthermore, if a value is used in a nested outlinable region, that value must always dominante that nested region by SSA construction. RFC: https://discourse.llvm.org/t/rfc-make-outlinable-openmp-operations-isolatedfromabove/90565 Fixes: #170781 Assisted-by: Codex
221 lines
8.6 KiB
C++
221 lines
8.6 KiB
C++
//===-- RematerializeFIRBoxOps.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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
// Some NoMemoryEffect FIR operations create allocas as an implementation detail
|
|
// of their conversion to the LLVM dialect. These allocas must not be
|
|
// accidentally shared across different threads when OpenMP outlining is used.
|
|
// This pass rematerializes selected operations into the outlined regions,
|
|
// which ensures that the allocas are correctly located inside of the outlined
|
|
// function.
|
|
//
|
|
// Operations rematerialized by this pass are re-created at each use inside of
|
|
// the affected regions. LLVM-IR CSE later in the pipeline should merge these
|
|
// where possible. However, CSE must not be run between this pass and the
|
|
// FIR-to-LLVM conversion because MLIR CSE will completely undo the actions of
|
|
// this pass. This is because the side effects on the FIR operations do not
|
|
// represent the side effects produced by their implementation in the LLVM
|
|
// dialect. This pass makes FIR-to-LLVM descriptor allocation safe for outlined
|
|
// regions.
|
|
|
|
#include "flang/Optimizer/CodeGen/CodeGen.h"
|
|
|
|
#include "flang/Optimizer/Dialect/FIRDialect.h"
|
|
#include "flang/Optimizer/Dialect/FIROps.h"
|
|
#include "mlir/Dialect/OpenMP/OpenMPDialect.h"
|
|
#include "mlir/IR/IRMapping.h"
|
|
#include "mlir/IR/PatternMatch.h"
|
|
#include "mlir/Interfaces/SideEffectInterfaces.h"
|
|
#include "llvm/ADT/DenseSet.h"
|
|
#include "llvm/ADT/STLExtras.h"
|
|
#include "llvm/ADT/SmallVector.h"
|
|
|
|
namespace fir {
|
|
#define GEN_PASS_DEF_REMATERIALIZEFIRBOXOPSPASS
|
|
#include "flang/Optimizer/CodeGen/CGPasses.h.inc"
|
|
} // namespace fir
|
|
|
|
namespace {
|
|
|
|
/// Returns true if \p op has a region that should be rematerialized into.
|
|
static bool isRematerializationRegionOp(mlir::Operation *op) {
|
|
return mlir::isa<mlir::omp::OutlineableOpenMPOpInterface>(op);
|
|
}
|
|
|
|
/// Return true for FIR box/class conversions whose operand may need to be
|
|
/// rematerialized while preserving the type expected by the use.
|
|
static bool isBoxConvert(mlir::Operation *op) {
|
|
auto convert = mlir::dyn_cast_or_null<fir::ConvertOp>(op);
|
|
return convert && mlir::isa<fir::BaseBoxType>(convert.getValue().getType()) &&
|
|
mlir::isa<fir::BaseBoxType>(convert.getType());
|
|
}
|
|
|
|
/// Return true if \p op should be cloned into rematerialization regions.
|
|
static bool shouldRematerialize(mlir::Operation *op) {
|
|
if (!op)
|
|
return false;
|
|
|
|
if (mlir::isa<fir::EmboxOp, fir::ReboxOp>(op)) {
|
|
assert(
|
|
mlir::isMemoryEffectFree(op) &&
|
|
"This transformation is not safe for operations with memory effects");
|
|
// Not all Embox and Rebox operations are speculatable. This should be safe
|
|
// because SSA can only express a use of the non-speculatable value inside
|
|
// of the region requiring rematerialization if that non-speculatable value
|
|
// always dominates the region. Therefore we are not adding any new UB from
|
|
// absent boxes/arguments. Furthermore, the newly rematerialized operations
|
|
// are created at the use site of the original value as a further guarantee
|
|
// that the rematerializations are only executed if the original value was
|
|
// executed.
|
|
return true;
|
|
}
|
|
|
|
// Rematerializing box-to-box conversions is safe and allows rematerialization
|
|
// of emboxes which are only used inside of the region through box-to-box
|
|
// conversions. Conversions to !fir.box<none> are common before calls to
|
|
// runtime functions.
|
|
if (isBoxConvert(op)) {
|
|
assert(
|
|
mlir::isPure(op) &&
|
|
"This transformation is not safe for operations with memory effects");
|
|
return shouldRematerialize(
|
|
mlir::cast<fir::ConvertOp>(op).getValue().getDefiningOp());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Return true if \p definingOp can be rematerialized into \p useRegion.
|
|
/// The use can be rematerialized if the defining operation is located
|
|
/// in an ancestor region of the use and the definition operation is
|
|
/// rematerializable.
|
|
static bool canRematerializeDefInRegion(mlir::Operation *definingOp,
|
|
mlir::Region &useRegion) {
|
|
if (!definingOp || !shouldRematerialize(definingOp))
|
|
return false;
|
|
|
|
mlir::Region *opRegion = definingOp->getParentRegion();
|
|
for (mlir::Region *ancestor = useRegion.getParentRegion(); ancestor;
|
|
ancestor = ancestor->getParentRegion()) {
|
|
if (opRegion == ancestor)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static mlir::Value cloneRematerializedValue(
|
|
mlir::IRRewriter &rewriter, mlir::Region &useRegion, mlir::Value value,
|
|
mlir::IRMapping &mapping,
|
|
llvm::SmallVectorImpl<mlir::Operation *> &eraseCandidates) {
|
|
if (mlir::Value mappedValue = mapping.lookupOrNull(value))
|
|
return mappedValue;
|
|
|
|
mlir::Operation *definingOp = value.getDefiningOp();
|
|
if (!canRematerializeDefInRegion(definingOp, useRegion))
|
|
return value;
|
|
|
|
// Clone rematerializable dependencies first so the cloned operation uses
|
|
// cloned operands when possible.
|
|
for (mlir::Value operand : definingOp->getOperands())
|
|
cloneRematerializedValue(rewriter, useRegion, operand, mapping,
|
|
eraseCandidates);
|
|
|
|
rewriter.clone(*definingOp, mapping);
|
|
eraseCandidates.push_back(definingOp);
|
|
return mapping.lookup(value);
|
|
}
|
|
|
|
struct RematerializationSite {
|
|
/// Operation whose operands will be rewritten. This is the insertion point
|
|
/// for the rematerialized operations.
|
|
mlir::Operation *user;
|
|
/// Operands of \c user that use values needing rematerialization.
|
|
llvm::SmallVector<mlir::OpOperand *> uses;
|
|
|
|
RematerializationSite(mlir::Operation *user,
|
|
llvm::SmallVector<mlir::OpOperand *> uses)
|
|
: user(user), uses(std::move(uses)) {}
|
|
};
|
|
|
|
/// Walk \p regionOwner and collect all rematerializable operands that use
|
|
/// values defined outside the region.
|
|
static void collectRematerializableUses(
|
|
mlir::Operation *regionOwner,
|
|
llvm::SmallVectorImpl<RematerializationSite> &rematSites) {
|
|
regionOwner->walk<mlir::WalkOrder::PreOrder>(
|
|
[&](mlir::Operation *op) -> mlir::WalkResult {
|
|
// Don't walk into nested rematerialization regions. They will be
|
|
// processed in their own calls to this function.
|
|
if (op != regionOwner && isRematerializationRegionOp(op))
|
|
return mlir::WalkResult::skip();
|
|
|
|
llvm::SmallVector<mlir::OpOperand *> rematerializableUses;
|
|
for (mlir::OpOperand &operand : op->getOpOperands()) {
|
|
if (canRematerializeDefInRegion(operand.get().getDefiningOp(),
|
|
*op->getParentRegion()))
|
|
rematerializableUses.push_back(&operand);
|
|
}
|
|
if (!rematerializableUses.empty())
|
|
rematSites.emplace_back(op, std::move(rematerializableUses));
|
|
|
|
return mlir::WalkResult::advance();
|
|
});
|
|
}
|
|
|
|
/// Rematerialize supported values defined outside of \p regionOwner into the
|
|
/// region
|
|
static void rematerializeInRegion(mlir::IRRewriter &rewriter,
|
|
mlir::Operation *regionOwner) {
|
|
llvm::SmallVector<RematerializationSite> rematSites;
|
|
collectRematerializableUses(regionOwner, rematSites);
|
|
if (rematSites.empty())
|
|
return;
|
|
|
|
mlir::OpBuilder::InsertionGuard guard(rewriter);
|
|
|
|
llvm::SmallVector<mlir::Operation *> eraseCandidates;
|
|
for (RematerializationSite &rematSite : rematSites) {
|
|
rewriter.setInsertionPoint(rematSite.user);
|
|
mlir::IRMapping mapping;
|
|
for (mlir::OpOperand *use : rematSite.uses) {
|
|
mlir::Region *useRegion = use->getOwner()->getParentRegion();
|
|
mlir::Value newValue = cloneRematerializedValue(
|
|
rewriter, *useRegion, use->get(), mapping, eraseCandidates);
|
|
use->set(newValue);
|
|
}
|
|
}
|
|
|
|
llvm::DenseSet<mlir::Operation *> erased;
|
|
for (mlir::Operation *op : llvm::reverse(eraseCandidates))
|
|
if (erased.insert(op).second && op->use_empty())
|
|
rewriter.eraseOp(op);
|
|
}
|
|
|
|
class RematerializeFIRBoxOpsPass
|
|
: public fir::impl::RematerializeFIRBoxOpsPassBase<
|
|
RematerializeFIRBoxOpsPass> {
|
|
public:
|
|
using RematerializeFIRBoxOpsPassBase<
|
|
RematerializeFIRBoxOpsPass>::RematerializeFIRBoxOpsPassBase;
|
|
|
|
void runOnOperation() override final {
|
|
mlir::Operation *top = getOperation();
|
|
|
|
llvm::SmallVector<mlir::Operation *> regionOwners;
|
|
top->walk([&](mlir::Operation *op) {
|
|
if (isRematerializationRegionOp(op))
|
|
regionOwners.push_back(op);
|
|
});
|
|
|
|
mlir::IRRewriter rewriter(top->getContext());
|
|
for (mlir::Operation *op : regionOwners)
|
|
rematerializeInRegion(rewriter, op);
|
|
}
|
|
};
|
|
|
|
} // namespace
|