| //===-- 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 |