//===- MapInfoFinalization.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 // //===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===// /// \file /// An OpenMP dialect related pass for FIR/HLFIR which performs some /// pre-processing of MapInfoOp's after the module has been lowered to /// finalize them. /// /// For example, it expands MapInfoOp's containing descriptor related /// types (fir::BoxType's) into multiple MapInfoOp's containing the parent /// descriptor and pointer member components for individual mapping, /// treating the descriptor type as a record type for later lowering in the /// OpenMP dialect. /// /// The pass also adds MapInfoOp's that are members of a parent object but are /// not directly used in the body of a target region to its BlockArgument list /// to maintain consistency across all MapInfoOp's tied to a region directly or /// indirectly via a parent object. //===----------------------------------------------------------------------===// #include "flang/Optimizer/Builder/DirectivesCommon.h" #include "flang/Optimizer/Builder/FIRBuilder.h" #include "flang/Optimizer/Builder/HLFIRTools.h" #include "flang/Optimizer/Dialect/FIRType.h" #include "flang/Optimizer/Dialect/Support/KindMapping.h" #include "flang/Optimizer/HLFIR/HLFIROps.h" #include "flang/Optimizer/OpenMP/Passes.h" #include "mlir/Analysis/SliceAnalysis.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/OpenMP/OpenMPDialect.h" #include "mlir/IR/BuiltinDialect.h" #include "mlir/IR/BuiltinOps.h" #include "mlir/IR/Operation.h" #include "mlir/IR/SymbolTable.h" #include "mlir/Pass/Pass.h" #include "mlir/Support/LLVM.h" #include "llvm/ADT/BitmaskEnum.h" #include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/StringSet.h" #include "llvm/Support/raw_ostream.h" #include #include #include #include #define DEBUG_TYPE "omp-map-info-finalization" namespace flangomp { #define GEN_PASS_DEF_MAPINFOFINALIZATIONPASS #include "flang/Optimizer/OpenMP/Passes.h.inc" } // namespace flangomp namespace { class MapInfoFinalizationPass : public flangomp::impl::MapInfoFinalizationPassBase< MapInfoFinalizationPass> { public: MapInfoFinalizationPass() = default; MapInfoFinalizationPass( const flangomp::MapInfoFinalizationPassOptions &options) : MapInfoFinalizationPassBase(options) {} /// Helper class tracking a members parent and its /// placement in the parents member list struct ParentAndPlacement { mlir::omp::MapInfoOp parent; size_t index; }; /// Tracks any intermediate function/subroutine local allocations we /// generate for the descriptors of box type dummy arguments, so that /// we can retrieve it for subsequent reuses within the functions /// scope. /// /// descriptor defining op /// | corresponding local alloca /// | | std::map localBoxAllocas; /// List of deferrable descriptors to process at the end of /// the pass and their associated attach map if it exists. llvm::SmallVector> deferrableDesc; /// List of base addresses already expanded from their /// descriptors within a parent, currently used to /// prevent incorrect member index generation. llvm::DenseMap> expandedBaseAddr; /// Return true if the given path exists in a list of paths. static bool containsPath(const llvm::SmallVectorImpl> &paths, llvm::ArrayRef path) { return llvm::any_of(paths, [&](const llvm::SmallVector &p) { return p.size() == path.size() && std::equal(p.begin(), p.end(), path.begin()); }); } /// Find a member MapInfoOp by its index path. /// \param op The parent MapInfoOp to search in /// \param indexPath The index path to find /// \return The member MapInfoOp if found, or null MapInfoOp if not found static mlir::omp::MapInfoOp findMemberByIndexPath(mlir::omp::MapInfoOp op, llvm::ArrayRef indexPath) { mlir::ArrayAttr attr = op.getMembersIndexAttr(); if (!attr) return mlir::omp::MapInfoOp(); size_t memberIdx = 0; for (mlir::Attribute list : attr) { auto listAttr = mlir::cast(list); if (listAttr.size() == indexPath.size()) { bool allEq = true; for (size_t j = 0; j < listAttr.size(); ++j) { auto indexAttr = mlir::cast(listAttr[j]); if (indexAttr.getInt() != indexPath[j]) { allEq = false; break; } } if (allEq) { if (auto memberOp = mlir::dyn_cast_if_present( op.getMembers()[memberIdx].getDefiningOp())) return memberOp; } } ++memberIdx; } return mlir::omp::MapInfoOp(); } /// Return true if the given path is already present in /// op.getMembersIndexAttr(). static bool mappedIndexPathExists(mlir::omp::MapInfoOp op, llvm::ArrayRef indexPath) { return findMemberByIndexPath(op, indexPath) != nullptr; } /// Get the map type of the nearest explicitly mapped parent for a member. /// "Explicitly mapped" means the map type does NOT have the implicit flag. /// /// \param parentOp The parent MapInfoOp containing members /// \param memberIndex The index path to the child member (e.g., [1, 2, 3]) /// \return The map type of the nearest explicitly mapped ancestor, or /// the parentOp's map type if no explicit ancestor is found. static mlir::omp::ClauseMapFlags getExplicitlyMappedParentMapType(mlir::omp::MapInfoOp parentOp, llvm::ArrayRef memberIndex) { llvm::SmallVector currentPath(memberIndex.begin(), memberIndex.end()); while (!currentPath.empty()) { if (auto memberOp = findMemberByIndexPath(parentOp, currentPath)) { if (!bitEnumContainsAll(memberOp.getMapType(), mlir::omp::ClauseMapFlags::implicit)) return memberOp.getMapType(); } currentPath.pop_back(); } return parentOp.getMapType(); } /// Build a compact string key for an index path for set-based /// deduplication. Format: "N:v0,v1,..." where N is the length. static void buildPathKey(llvm::ArrayRef path, llvm::SmallString<64> &outKey) { outKey.clear(); llvm::raw_svector_ostream os(outKey); os << path.size() << ':'; for (size_t i = 0; i < path.size(); ++i) { if (i) os << ','; os << path[i]; } } /// Return true if the module has an OpenMP requires clause that includes /// unified_shared_memory. static bool moduleRequiresUSM(mlir::ModuleOp module) { assert(module && "invalid module"); if (auto req = module->getAttrOfType( "omp.requires")) return mlir::omp::bitEnumContainsAll( req.getValue(), mlir::omp::ClauseRequires::unified_shared_memory); return false; } /// Create the member map for coordRef and append it (and its index /// path) to the provided new* vectors, if it is not already present. void appendMemberMapIfNew( mlir::omp::MapInfoOp op, fir::FirOpBuilder &builder, mlir::Location loc, mlir::Value coordRef, llvm::ArrayRef indexPath, llvm::StringRef memberName, llvm::SmallVectorImpl &newMapOpsForFields, llvm::SmallVectorImpl> &newMemberIndexPaths) { // Local de-dup within this op invocation. if (containsPath(newMemberIndexPaths, indexPath)) return; // Global de-dup against already present member indices. if (mappedIndexPathExists(op, indexPath)) return; if (op.getMapperId()) { mlir::omp::DeclareMapperOp symbol = mlir::SymbolTable::lookupNearestSymbolFrom< mlir::omp::DeclareMapperOp>(op, op.getMapperIdAttr()); assert(symbol && "missing symbol for declare mapper identifier"); mlir::omp::DeclareMapperInfoOp mapperInfo = symbol.getDeclareMapperInfo(); // TODO: Probably a way to cache these keys in someway so we don't // constantly go through the process of rebuilding them on every check, to // save some cycles, but it can wait for a subsequent patch. for (auto v : mapperInfo.getMapVars()) { mlir::omp::MapInfoOp map = mlir::cast(v.getDefiningOp()); if (!map.getMembers().empty() && mappedIndexPathExists(map, indexPath)) return; } } builder.setInsertionPoint(op); fir::factory::AddrAndBoundsInfo info = fir::factory::getDataOperandBaseAddr( builder, coordRef, /*isOptional=*/false, loc); llvm::SmallVector bounds = fir::factory::genImplicitBoundsOps< mlir::omp::MapBoundsOp, mlir::omp::MapBoundsType>( builder, info, hlfir::translateToExtendedValue(loc, builder, hlfir::Entity{coordRef}) .first, /*dataExvIsAssumedSize=*/false, loc); // Get the map type from the nearest explicitly mapped parent, falling back // to the top-level parent's map type if no explicit ancestor is found. mlir::omp::ClauseMapFlags mapType = getExplicitlyMappedParentMapType(op, indexPath); mlir::omp::MapInfoOp fieldMapOp = mlir::omp::MapInfoOp::create( builder, loc, coordRef.getType(), coordRef, mlir::TypeAttr::get(fir::unwrapRefType(coordRef.getType())), builder.getAttr(mapType), builder.getAttr( mlir::omp::VariableCaptureKind::ByRef), /*varPtrPtr=*/mlir::Value{}, /*varPtrPtr=*/mlir::TypeAttr{}, /*members=*/mlir::ValueRange{}, /*members_index=*/mlir::ArrayAttr{}, bounds, /*mapperId=*/mlir::FlatSymbolRefAttr(), builder.getStringAttr(op.getNameAttr().strref() + "." + memberName + ".implicit_map"), /*partial_map=*/builder.getBoolAttr(false)); newMapOpsForFields.emplace_back(fieldMapOp); newMemberIndexPaths.emplace_back(indexPath.begin(), indexPath.end()); } // Check if the declaration operation we have refers to a dummy // function argument. bool isDummyArgument(mlir::Value mappedValue) { if (auto declareOp = mlir::dyn_cast_if_present( mappedValue.getDefiningOp())) if (auto dummyScope = declareOp.getDummyScope()) return true; return false; } // Relevant for OpenMP < 5.2, where attach semantics and rules don't exist. // As descriptors were an unspoken implementation detail in these versions // there's certain cases where the user (and the compiler implementation) // can create data mapping errors by having temporary descriptors stuck // in memory. The main example is calling an 'target enter data map' // without a corresponding exit on an assumed shape or size dummy // argument, a local stack descriptor is generated, gets mapped and // is then left on device. A user doesn't realize what they've done as // the OpenMP specification isn't explicit on descriptor handling in // earlier versions and as far as Fortran is concerned this si something // hidden from a user. To avoid this we can defer the descriptor mapping // in these cases until target or target data regions, when we can be // sure they have a clear limited scope on device. bool canDeferDescriptorMapping(mlir::Value descriptor) { if (fir::isAllocatableType(descriptor.getType()) || fir::isPointerType(descriptor.getType())) return false; if (isDummyArgument(descriptor) && (fir::isAssumedType(descriptor.getType()) || fir::isAssumedShape(descriptor.getType()))) return true; return false; } /// getMemberUserList gathers all users of a particular MapInfoOp that are /// other MapInfoOp's and places them into the mapMemberUsers list, which /// records the map that the current argument MapInfoOp "op" is part of /// alongside the placement of "op" in the recorded users members list. The /// intent of the generated list is to find all MapInfoOp's that may be /// considered parents of the passed in "op" and in which it shows up in the /// member list, alongside collecting the placement information of "op" in its /// parents member list. void getMemberUserList(mlir::omp::MapInfoOp op, llvm::SmallVectorImpl &mapMemberUsers) { for (auto *user : op->getUsers()) if (auto map = mlir::dyn_cast_if_present(user)) for (auto [i, mapMember] : llvm::enumerate(map.getMembers())) if (mapMember.getDefiningOp() == op) mapMemberUsers.push_back({map, i}); } void getAsIntegers(llvm::ArrayRef values, llvm::SmallVectorImpl &ints) { ints.reserve(values.size()); llvm::transform(values, std::back_inserter(ints), [](mlir::Attribute value) { return mlir::cast(value).getInt(); }); } /// This function will expand a MapInfoOp's member indices back into a vector /// so that they can be trivially modified as unfortunately the attribute type /// that's used does not have modifiable fields at the moment (generally /// awkward to work with) void getMemberIndicesAsVectors( mlir::omp::MapInfoOp mapInfo, llvm::SmallVectorImpl> &indices) { indices.reserve(mapInfo.getMembersIndexAttr().getValue().size()); llvm::transform(mapInfo.getMembersIndexAttr().getValue(), std::back_inserter(indices), [this](mlir::Attribute value) { auto memberIndex = mlir::cast(value); llvm::SmallVector indexes; getAsIntegers(memberIndex.getValue(), indexes); return indexes; }); } // Check if the declaration operation we have refers to a dummy // function argument. bool isDummyArgument(mlir::Operation *op) { if (auto declareOp = mlir::dyn_cast(op)) if (auto dummyScope = declareOp.getDummyScope()) return true; return false; } /// When provided a MapInfoOp containing a descriptor type that /// we must expand into multiple maps this function will extract /// the value from it and return it, in certain cases we must /// generate a new allocation to store into so that the /// fir::BoxOffsetOp we utilise to access the descriptor datas /// base address can be utilised. mlir::Value getDescriptorFromBoxMap(mlir::omp::MapInfoOp boxMap, fir::FirOpBuilder &builder, bool &canDescBeDeferred) { mlir::Value descriptor = boxMap.getVarPtr(); if (!fir::isTypeWithDescriptor(boxMap.getVarPtrType())) if (auto addrOp = mlir::dyn_cast_if_present( boxMap.getVarPtr().getDefiningOp())) descriptor = addrOp.getVal(); canDescBeDeferred = canDeferDescriptorMapping(descriptor); if ((!mlir::isa(descriptor.getType()) && !fir::factory::isOptionalArgument(descriptor.getDefiningOp()))) return descriptor; mlir::Value &alloca = localBoxAllocas[descriptor.getDefiningOp()]; mlir::Location loc = boxMap->getLoc(); if (!alloca) { // The fir::BoxOffsetOp only works with !fir.ref> types, as // allowing it to access non-reference box operations can cause some // problematic SSA IR. However, in the case of assumed shape's the type // is not a !fir.ref, in these cases to retrieve the appropriate // !fir.ref> to access the data we need to map we must // perform an alloca and then store to it and retrieve the data from the // new alloca. mlir::OpBuilder::InsertPoint insPt = builder.saveInsertionPoint(); mlir::Block *allocaBlock = builder.getAllocaBlock(); assert(allocaBlock && "No alloca block found for this top level op"); builder.setInsertionPointToStart(allocaBlock); mlir::Type allocaType = descriptor.getType(); if (fir::isBoxAddress(allocaType)) allocaType = fir::unwrapRefType(allocaType); alloca = fir::AllocaOp::create(builder, loc, allocaType); builder.restoreInsertionPoint(insPt); } // We should only emit a store if the passed in data is present, it is // possible a user passes in no argument to an optional parameter, in which // case we cannot store or we'll segfault on the emitted memcpy. // TODO: We currently emit a present -> load/store every time we use a // mapped value that requires a local allocation, this isn't the most // efficient, although, it is more correct in a lot of situations. One // such situation is emitting a this series of instructions in separate // segments of a branch (e.g. two target regions in separate else/if branch // mapping the same function argument), however, it would be nice to be able // to optimize these situations e.g. raising the load/store out of the // branch if possible. But perhaps this is best left to lower level // optimisation passes. auto isPresent = fir::IsPresentOp::create(builder, loc, builder.getI1Type(), descriptor); builder.genIfOp(loc, {}, isPresent, false) .genThen([&]() { descriptor = builder.loadIfRef(loc, descriptor); fir::StoreOp::create(builder, loc, descriptor, alloca); }) .end(); return alloca; } mlir::omp::ClauseMapFlags removeAttachModifiers(mlir::omp::ClauseMapFlags mapType) { // We can remove these maps as the lowering to LLVM-IR and the runtime have // no requirement for these, it's primarily an indicator for this and // similar passes. This is of course subject to change if we find need // for it. mapType &= ~mlir::omp::ClauseMapFlags::attach_always; mapType &= ~mlir::omp::ClauseMapFlags::attach_never; mapType &= ~mlir::omp::ClauseMapFlags::attach_auto; return mapType; } /// Function that generates a FIR operation accessing the descriptor's /// base address (BoxOffsetOp) and a MapInfoOp for it. The most /// important thing to note is that we normally move the bounds from /// the descriptor map onto the base address map. /// /// \p parentOp is the MapInfoOp being expanded (the descriptor map before /// this pass splits it). Lowering attaches a NameLoc there for the Fortran /// map text. New ops created here use its location so NameLoc is preserved. mlir::omp::MapInfoOp genBaseAddrMap(mlir::Location mapInfoOpLoc, mlir::Value descriptor, mlir::omp::MapInfoOp parentOp, mlir::omp::ClauseMapFlags mapType, fir::FirOpBuilder &builder, bool isRefPtee = false, mlir::FlatSymbolRefAttr mapperId = mlir::FlatSymbolRefAttr()) { mlir::Value baseAddr = fir::BoxOffsetOp::create( builder, mapInfoOpLoc, descriptor, fir::BoxFieldAttr::base_addr); mlir::Type underlyingBaseAddrType = llvm::cast( fir::unwrapRefType(baseAddr.getType())) .getElementType(); if (auto seqType = llvm::dyn_cast(underlyingBaseAddrType)) if (seqType.hasDynamicExtents()) underlyingBaseAddrType = seqType.getEleTy(); mlir::Type underlyingDescType = fir::unwrapRefType(descriptor.getType()); // Member of the descriptor pointing at the allocated data return mlir::omp::MapInfoOp::create( builder, mapInfoOpLoc, baseAddr.getType(), descriptor, mlir::TypeAttr::get(underlyingDescType), builder.getAttr( removeAttachModifiers(mapType)), builder.getAttr( mlir::omp::VariableCaptureKind::ByRef), baseAddr, mlir::TypeAttr::get(underlyingBaseAddrType), isRefPtee ? parentOp.getMembers() : mlir::SmallVector{}, isRefPtee ? parentOp.getMembersIndexAttr() : mlir::ArrayAttr{}, parentOp.getBounds(), /*mapperId=*/mapperId, /*name=*/builder.getStringAttr(""), /*partial_map=*/builder.getBoolAttr(false)); } /// This function adjusts the member indices vector to include a new /// base address member. We take the position of the descriptor in /// the member indices list, which is the index data that the base /// addresses index will be based off of, as the base address is /// a member of the descriptor. We must also alter other members /// that are members of this descriptor to account for the addition /// of the base address index. void adjustMemberIndices( llvm::SmallVectorImpl> &memberIndices, ParentAndPlacement parentAndPlacement) { llvm::SmallVector baseAddrIndex = memberIndices[parentAndPlacement.index]; auto &expansionIndices = expandedBaseAddr[parentAndPlacement.parent]; // If we find another member that is "derived/a member of" the descriptor // that is not the descriptor itself, we must insert a 0 for the new base // address we have just added for the descriptor into the list at the // appropriate position to maintain correctness of the positional/index data // for that member. for (auto [i, member] : llvm::enumerate(memberIndices)) { if (expansionIndices.contains(i)) if (member.size() == baseAddrIndex.size() + 1 && member[baseAddrIndex.size()] == 0) continue; if (member.size() > baseAddrIndex.size() && std::equal(baseAddrIndex.begin(), baseAddrIndex.end(), member.begin())) member.insert(std::next(member.begin(), baseAddrIndex.size()), 0); } // Add the base address index to the main base address member data baseAddrIndex.push_back(0); uint64_t newIdxInsert = parentAndPlacement.index + 1; expansionIndices.insert(newIdxInsert); // Insert our newly created baseAddrIndex into the larger list of // indices at the correct location. memberIndices.insert(std::next(memberIndices.begin(), newIdxInsert), baseAddrIndex); } /// This function takes a Map clause owning target operation (e.g. TargetOp /// or TargetDataOp) and a function to be invoked on the various map-like /// clause ranges of that target operation (e.g. use_device_ptr/addr, map, /// etc) with the intent of inserting new maps into the range in a manner /// that is consistent with the target that was passed in. /// /// \param target - The OpenMP target directive operation owning the map or /// map-like clause lists. /// \param addOperands - A lambda function that should take 3 parameters: /// a range that represents the map range, an operation representing /// the target and an unsigned integer representing the start index /// for the map range in terms of the targets block argument list. void insertIntoMapClauseInterface(mlir::Operation *target, std::function addOperands) { auto argIface = llvm::dyn_cast(target); if (auto mapClauseOwner = llvm::dyn_cast(target)) { mlir::MutableOperandRange mapVarsArr = mapClauseOwner.getMapVarsMutable(); unsigned blockArgInsertIndex = argIface ? argIface.getMapBlockArgsStart() + argIface.numMapBlockArgs() : 0; addOperands(mapVarsArr, llvm::dyn_cast_if_present(target), blockArgInsertIndex); } if (auto targetDataOp = llvm::dyn_cast(target)) { mlir::MutableOperandRange useDevAddrMutableOpRange = targetDataOp.getUseDeviceAddrVarsMutable(); addOperands(useDevAddrMutableOpRange, target, argIface.getUseDeviceAddrBlockArgsStart() + argIface.numUseDeviceAddrBlockArgs()); mlir::MutableOperandRange useDevPtrMutableOpRange = targetDataOp.getUseDevicePtrVarsMutable(); addOperands(useDevPtrMutableOpRange, target, argIface.getUseDevicePtrBlockArgsStart() + argIface.numUseDevicePtrBlockArgs()); } else if (auto targetOp = llvm::dyn_cast(target)) { mlir::MutableOperandRange hasDevAddrMutableOpRange = targetOp.getHasDeviceAddrVarsMutable(); addOperands(hasDevAddrMutableOpRange, target, argIface.getHasDeviceAddrBlockArgsStart() + argIface.numHasDeviceAddrBlockArgs()); } } // This functions aims to insert a new attach maps derived from existing maps // into the corresponding clause list, interlinking it correctly with block // arguments where required. It only inserts these into the map lists, no // other type of Map clause-like list receives attach maps regardless of the // original list of the map it's derived from. void addAttachMemberToTarget( mlir::omp::MapInfoOp owner, mlir::omp::MapInfoOp derived, llvm::SmallVectorImpl &mapMemberUsers, fir::FirOpBuilder &builder, mlir::Operation *target) { auto addOperands = [&](mlir::MutableOperandRange &mapVarsArr, mlir::Operation *directiveOp, unsigned blockArgInsertIndex = 0) { // Check we're not inserting a duplicate map. if (llvm::is_contained(mapVarsArr.getAsOperandRange(), derived.getResult())) return; mapVarsArr.append(mlir::ValueRange{derived}); if (directiveOp) { directiveOp->getRegion(0).insertArgument( blockArgInsertIndex, derived.getType(), derived.getLoc()); blockArgInsertIndex++; } }; auto argIface = llvm::dyn_cast(target); if (auto mapClauseOwner = llvm::dyn_cast(target)) { mlir::MutableOperandRange mapVarsArr = mapClauseOwner.getMapVarsMutable(); unsigned blockArgInsertIndex = argIface ? argIface.getMapBlockArgsStart() + argIface.numMapBlockArgs() : 0; addOperands(mapVarsArr, llvm::dyn_cast_if_present( argIface.getOperation()), blockArgInsertIndex); } } // We add all mapped record members not directly used in the target region // to the block arguments in front of their parent and we place them into // the map operands list for consistency. // // These indirect uses (via accesses to their parent) will still be // mapped individually in most cases, and a parent mapping doesn't // guarantee the parent will be mapped in its totality, partial // mapping is common. // // For example: // map(tofrom: x%y) // // Will generate a mapping for "x" (the parent) and "y" (the member). // The parent "x" will not be mapped, but the member "y" will. // However, we must have the parent as a BlockArg and MapOperand // in these cases, to maintain the correct uses within the region and // to help tracking that the member is part of a larger object. // // In the case of: // map(tofrom: x%y, x%z) // // The parent member becomes more critical, as we perform a partial // structure mapping where we link the mapping of the members y // and z together via the parent x. We do this at a kernel argument // level in LLVM IR and not just MLIR, which is important to maintain // similarity to Clang and for the runtime to do the correct thing. // However, we still do not map the structure in its totality but // rather we generate an un-sized "binding" map entry for it. // // In the case of: // map(tofrom: x, x%y, x%z) // // We do actually map the entirety of "x", so the explicit mapping of // x%y, x%z becomes unnecessary, except in cases where y or z are // pointers. void addImplicitMembersToTarget(mlir::omp::MapInfoOp op, fir::FirOpBuilder &builder, mlir::Operation *target) { // TargetDataOp is technically a MapClauseOwningOpInterface, so we // do not need to explicitly check for the extra cases here for use_device // addr/ptr. if (!llvm::isa_and_present(target)) return; auto addOperands = [&](mlir::MutableOperandRange &mapVarsArr, mlir::Operation *directiveOp, unsigned blockArgInsertIndex = 0) { if (!llvm::is_contained(mapVarsArr.getAsOperandRange(), op.getResult())) return; for (auto mapMember : op.getMembers()) { if (llvm::is_contained(mapVarsArr.getAsOperandRange(), mapMember)) continue; mapVarsArr.append(mlir::ValueRange{mapMember}); if (directiveOp) { directiveOp->getRegion(0).insertArgument( blockArgInsertIndex, mapMember.getType(), mapMember.getLoc()); blockArgInsertIndex++; } } }; insertIntoMapClauseInterface(target, addOperands); } bool verifyUsesConstraint(mlir::omp::MapInfoOp op) { if (llvm::hasSingleElement(op->getUsers())) return true; if (llvm::range_size(op->getUsers()) > 2) return false; // We only allow a TargetOp or MapInfoOp when we have multiple users // for the moment. bool targetUser = false; for (auto *user : op->getUsers()) { if (targetUser && !llvm::isa( user)) return false; // We do not handle multiple target users currently. if (targetUser && llvm::isa( user)) return false; if (!targetUser) targetUser = llvm::isa(user); } return true; } // We retrieve the first user that is a Target operation, of which // there should only be one currently. Every MapInfoOp can be tied to // at most one Target operation and at the minimum no operations. // This may change in the future with IR cleanups/modifications, // in which case this pass will need updating to support cases // where a map can have more than one user and more than one of // those users can be a Target operation. For now, we simply // return the first target operation encountered, which may // be on the parent MapInfoOp in the case of a member mapping. // In that case, we traverse the MapInfoOp chain until we // find the first TargetOp user. mlir::Operation *getFirstTargetUser(mlir::omp::MapInfoOp mapOp) { assert(verifyUsesConstraint(mapOp) && "OMPMapInfoFinalization currently only supports " "single users or up to two users when those users" "are a MapInfoOp and Target mapping directive"); for (auto *user : mapOp->getUsers()) { if (llvm::isa(user)) return user; if (auto mapUser = llvm::dyn_cast(user)) return getFirstTargetUser(mapUser); } return nullptr; } /// Adjusts the descriptor's map type. The main alteration that is done /// currently is transforming the map type to `OMP_MAP_TO` where possible. /// This is because we will always need to map the descriptor to device /// (or at the very least it seems to be the case currently with the /// current lowered kernel IR), as without the appropriate descriptor /// information on the device there is a risk of the kernel IR /// requesting for various data that will not have been copied to /// perform things like indexing. This can cause segfaults and /// memory access errors. However, we do not need this data mapped /// back to the host from the device, as per the OpenMP spec we cannot alter /// the data via resizing or deletion on the device. Discarding any /// descriptor alterations via no map back is reasonable (and required /// for certain segments of descriptor data like the type descriptor that are /// global constants). This alteration is only inapplicable to `target exit` /// and `target update` currently, and that's due to `target exit` not /// allowing `to` mappings, and `target update` not allowing both `to` and /// `from` simultaneously. We currently try to maintain the `implicit` flag /// where necessary, although it does not seem strictly required. /// /// Currently, if it is a has_device_addr clause, we opt to not apply the /// descriptor tag to it as it's used differently to a regular mapping /// and some of the runtime descriptor behaviour at the moment can cause /// issues. mlir::omp::ClauseMapFlags getDescriptorMapType(mlir::omp::ClauseMapFlags mapTypeFlag, mlir::Operation *target) { using mapFlags = mlir::omp::ClauseMapFlags; mapFlags flags = mapFlags::none; if (llvm::isa_and_nonnull(target)) { flags |= mapTypeFlag | mapFlags::descriptor; return flags; } flags |= mapFlags::to | mapFlags::descriptor | mapFlags::always | (mapTypeFlag & mapFlags::implicit); if (moduleRequiresUSM(target->getParentOfType())) flags |= mapFlags::close; return flags; } /// Check if the mapOp is present in the HasDeviceAddr clause on /// the userOp. Only applies to TargetOp. bool isHasDeviceAddr(mlir::omp::MapInfoOp mapOp, mlir::Operation &userOp) { if (auto targetOp = llvm::dyn_cast(userOp)) { for (mlir::Value hda : targetOp.getHasDeviceAddrVars()) { if (hda.getDefiningOp() == mapOp) return true; } } return false; } bool isUseDeviceAddr(mlir::omp::MapInfoOp mapOp, mlir::Operation &userOp) { if (auto targetDataOp = llvm::dyn_cast(userOp)) { for (mlir::Value uda : targetDataOp.getUseDeviceAddrVars()) { if (uda.getDefiningOp() == mapOp) return true; } } return false; } bool isUseDevicePtr(mlir::omp::MapInfoOp mapOp, mlir::Operation &userOp) { if (auto targetDataOp = llvm::dyn_cast(userOp)) { for (mlir::Value udp : targetDataOp.getUseDevicePtrVars()) { if (udp.getDefiningOp() == mapOp) return true; } } return false; } /// This function generates an attach map, which is an type of OpenMP map that /// binds a pointer to its data. In the case of Fortran, this binding is /// primarily for binding the pointer inside of descriptors to the underlying /// data pointed to by the descriptor. This is simply an extra map that we /// must emit when generating any map of a descriptor type be it ref_ptr, /// ref_ptee or ref_ptr + ref_ptee (ref_ptr_ptee in OpenMP parlance), to bind /// the pointer inside of the descriptor to its respective data. The only case /// this can be omitted is when a user has explicitly asked for different /// attach semantics e.g. specifying attach(none) as a map modifier. This is /// the case where the [[maybe_unused]] attribute is relevant. [[maybe_unused]] mlir::Operation *genImplicitAttachMap( mlir::omp::MapInfoOp descMapOp, mlir::Value descriptor, llvm::SmallVectorImpl &mapMemberUsers, mlir::Operation *target, fir::FirOpBuilder &builder, mlir::omp::ClauseMapFlags refFlagType, bool isAttachAlways = false, mlir::Value reuseBaseAddr = mlir::Value{}) { auto baseAddr = reuseBaseAddr ? reuseBaseAddr : fir::BoxOffsetOp::create(builder, descMapOp->getLoc(), descriptor, fir::BoxFieldAttr::base_addr); mlir::Type underlyingVarType = llvm::cast( fir::unwrapRefType(baseAddr.getType())) .getElementType(); if (auto seqType = llvm::dyn_cast(underlyingVarType)) if (seqType.hasDynamicExtents()) underlyingVarType = seqType.getEleTy(); auto implicitAttachMap = mlir::omp::MapInfoOp::create( builder, descMapOp->getLoc(), descMapOp.getResult().getType(), descriptor, mlir::TypeAttr::get(fir::unwrapRefType(descriptor.getType())), builder.getAttr( mlir::omp::ClauseMapFlags::attach | refFlagType | (isAttachAlways ? mlir::omp::ClauseMapFlags::always : mlir::omp::ClauseMapFlags::none)), descMapOp.getMapCaptureTypeAttr(), /*varPtrPtr=*/ baseAddr, mlir::TypeAttr::get(underlyingVarType), /*members=*/mlir::SmallVector{}, /*membersIndex=*/mlir::ArrayAttr{}, /*bounds=*/descMapOp.getBounds(), /*mapperId*/ mlir::FlatSymbolRefAttr(), descMapOp.getNameAttr(), /*partial_map=*/builder.getBoolAttr(false)); // Has to be added to the target immediately, as we expect all maps // processed by this pass to have a user that is a target. addAttachMemberToTarget(descMapOp, implicitAttachMap, mapMemberUsers, builder, target); return implicitAttachMap; } // Expand mappings of type(C_PTR) to map their `__address` field explicitly // as a single pointer-sized member (USM-gated at callsite). This helps in // USM scenarios to ensure the pointer-sized mapping is used. mlir::omp::MapInfoOp genCptrMemberMap(mlir::omp::MapInfoOp op, fir::FirOpBuilder &builder) { if (!op.getMembers().empty()) return op; mlir::Type varTy = fir::unwrapRefType(op.getVarPtr().getType()); if (!mlir::isa(varTy)) return op; auto recTy = mlir::cast(varTy); // If not a builtin C_PTR record, skip. if (!recTy.getName().ends_with("__builtin_c_ptr")) return op; // Find the index of the c_ptr address component named "__address". int32_t fieldIdx = recTy.getFieldIndex("__address"); if (fieldIdx < 0) return op; mlir::Location loc = op.getVarPtr().getLoc(); mlir::Type memTy = recTy.getType(fieldIdx); fir::IntOrValue idxConst = mlir::IntegerAttr::get(builder.getI32Type(), fieldIdx); mlir::Value coord = fir::CoordinateOp::create( builder, loc, builder.getRefType(memTy), op.getVarPtr(), llvm::SmallVector{idxConst}); // Child for the `__address` member. llvm::SmallVector> memberIdx = {{0}}; mlir::ArrayAttr newMembersAttr = builder.create2DI64ArrayAttr(memberIdx); // Force CLOSE in USM paths so the pointer gets device-local placement // when required by tests relying on USM + close semantics. mlir::omp::ClauseMapFlagsAttr mapTypeAttr = builder.getAttr( op.getMapType() | mlir::omp::ClauseMapFlags::close); mlir::omp::MapInfoOp memberMap = mlir::omp::MapInfoOp::create( builder, loc, coord.getType(), coord, mlir::TypeAttr::get(fir::unwrapRefType(coord.getType())), mapTypeAttr, builder.getAttr( mlir::omp::VariableCaptureKind::ByRef), /*varPtrPtr=*/mlir::Value{}, /*varPtrPtrType=*/mlir::TypeAttr{}, /*members=*/llvm::SmallVector{}, /*member_index=*/mlir::ArrayAttr{}, /*bounds=*/op.getBounds(), /*mapperId=*/mlir::FlatSymbolRefAttr(), /*name=*/op.getNameAttr(), /*partial_map=*/builder.getBoolAttr(false)); // Rebuild the parent as a container with the `__address` member. mlir::omp::MapInfoOp newParent = mlir::omp::MapInfoOp::create( builder, op.getLoc(), op.getResult().getType(), op.getVarPtr(), op.getVarPtrTypeAttr(), mapTypeAttr, op.getMapCaptureTypeAttr(), /*varPtrPtr=*/mlir::Value{}, mlir::TypeAttr{}, /*members=*/llvm::SmallVector{memberMap}, /*member_index=*/newMembersAttr, /*bounds=*/llvm::SmallVector{}, /*mapperId=*/mlir::FlatSymbolRefAttr(), op.getNameAttr(), /*partial_map=*/builder.getBoolAttr(false)); op.replaceAllUsesWith(newParent.getResult()); op->erase(); return newParent; } // If the operation that we are expanding with a descriptor has a user // (parent), then we have to expand the parent's member indices to reflect // the adjusted member indices for the base address insertion. However, if // it does not then we are expanding a MapInfoOp without any pre-existing // member information to now have one new member for the base address, or // we are expanding a parent that is a descriptor and we have to adjust // all of its members to reflect the insertion of the base address. // // If we're expanding a top-level descriptor for a map operation that // resulted from "has_device_addr" clause, then we want the base pointer // from the descriptor to be used verbatim, i.e. without additional // remapping. To avoid this remapping, simply don't generate any map // information for the descriptor members. void createBaseAddrInsertion( fir::FirOpBuilder &builder, mlir::omp::MapInfoOp parentOp, mlir::omp::MapInfoOp baseAddr, llvm::SmallVectorImpl &mapMemberUsers, mlir::ArrayAttr &newMembersAttr, mlir::SmallVectorImpl &newMembers, llvm::SmallVector> &memberIndices) { if (!mapMemberUsers.empty()) { // Currently, there should only be one user per map when this pass // is executed. Either a parent map, holding the current map in its // member list, or a target operation that holds a map clause. This // may change in the future if we aim to refactor the MLIR for map // clauses to allow sharing of duplicate maps across target // operations. assert(mapMemberUsers.size() == 1 && "OMPMapInfoFinalization currently only supports single users of a " "MapInfoOp"); ParentAndPlacement mapUser = mapMemberUsers[0]; adjustMemberIndices(memberIndices, mapUser); llvm::SmallVector newMemberOps; for (auto v : mapUser.parent.getMembers()) { newMemberOps.push_back(v); if (v == parentOp) newMemberOps.push_back(baseAddr); } mapUser.parent.getMembersMutable().assign(newMemberOps); mapUser.parent.setMembersIndexAttr( builder.create2DI64ArrayAttr(memberIndices)); } else { newMembers.push_back(baseAddr); if (!parentOp.getMembers().empty()) { for (auto &indices : memberIndices) indices.insert(indices.begin(), 0); memberIndices.insert(memberIndices.begin(), {0}); newMembersAttr = builder.create2DI64ArrayAttr(memberIndices); newMembers.append(parentOp.getMembers().begin(), parentOp.getMembers().end()); } else { llvm::SmallVector> memberIdx = {{0}}; newMembersAttr = builder.create2DI64ArrayAttr(memberIdx); } } } /// Helper function to generate a ref_ptr map. This handles the case where /// we have a descriptor that should only map the pointer (descriptor) itself, /// without mapping the pointed-to data. /// /// For ref_ptr, we generate a map of the descriptor with user specified /// map types and, in the default auto attach case, we generate an /// additional attach map which indicates to the runtime to try and attach /// the base address to the descriptor if it's available and it's the first /// time the ref_ptr has been allocated on the device. void genRefPtrMap(mlir::omp::MapInfoOp op, fir::FirOpBuilder &builder, mlir::Operation *target, mlir::Value descriptor, llvm::SmallVectorImpl &mapMemberUsers, bool isAttachNever, bool isAttachAlways) { auto newMapInfoOp = mlir::omp::MapInfoOp::create( builder, op->getLoc(), op.getResult().getType(), descriptor, mlir::TypeAttr::get(fir::unwrapRefType(descriptor.getType())), builder.getAttr(op.getMapType()), op.getMapCaptureTypeAttr(), /*varPtrPtr=*/op.getVarPtrPtr(), /*varPtrPtrType=*/op.getVarPtrPtrTypeAttr(), op.getMembers(), op.getMembersIndexAttr(), /*bounds=*/mlir::SmallVector{}, /*mapperId*/ mlir::FlatSymbolRefAttr(), op.getNameAttr(), /*partial_map=*/builder.getBoolAttr(false)); if (!isAttachNever) genImplicitAttachMap(op, descriptor, mapMemberUsers, target, builder, mlir::omp::ClauseMapFlags::ref_ptr, isAttachAlways); op.replaceAllUsesWith(newMapInfoOp.getResult()); op->erase(); } /// Helper function to generate a ref_ptee map. This handles the case where /// we have a descriptor but should only map the pointed-to data (pointee), /// not the descriptor itself. /// /// For ref_ptee, we generate a map of the base address with user specified /// map types and, in the default auto attach case, we generate an /// additional attach map which indicates to the runtime to try and attach /// the base address to the descriptor if it's available and it's the first /// time the ref_ptee has been allocated on the device. void genRefPteeMap(mlir::omp::MapInfoOp op, fir::FirOpBuilder &builder, mlir::Operation *target, mlir::Value descriptor, llvm::SmallVectorImpl &mapMemberUsers, bool isAttachNever, bool isAttachAlways, mlir::FlatSymbolRefAttr mapperId) { // NOTE: We replace the descriptor map with the base address map. This // effectively replaces the descriptor's index position in any complex // structure mapping. This is a little different to the // ref_ptr_ptee/default map case, where we effectively insert a new member // with its own index position and have to nudge all children down an // index. This should be fine but it's worth noting the oddity in case // issues do pop up. auto newMapInfoOp = genBaseAddrMap(op.getLoc(), descriptor, op, op.getMapType(), builder, /*IsRefPtee=*/true, mapperId); if (!isAttachNever) genImplicitAttachMap(op, descriptor, mapMemberUsers, target, builder, mlir::omp::ClauseMapFlags::ref_ptee, isAttachAlways, newMapInfoOp.getVarPtrPtr()); op.replaceAllUsesWith(newMapInfoOp.getResult()); op->erase(); } /// Helper function to generate a ref_ptr_ptee or default descriptor map. /// This is the standard descriptor mapping that maps both the descriptor /// and its base address/pointed-to data. /// /// For ref_ptr_ptee, it combines both ref_ptr and ref_ptee behavior: /// a map is generated for the descriptor and its base address, /// similarly in the default auto attach case, we generate an additional /// attach map. void genRefPtrPteeOrDefaultMap( mlir::omp::MapInfoOp op, fir::FirOpBuilder &builder, mlir::Operation *target, mlir::Value descriptor, llvm::SmallVectorImpl &mapMemberUsers, bool isAttachNever, bool isAttachAlways, bool isHasDeviceAddrFlag, bool descCanBeDeferred, mlir::FlatSymbolRefAttr mapperId) { bool isRefPtrPtee = bitEnumContainsAll(op.getMapType(), mlir::omp::ClauseMapFlags::ref_ptr) && bitEnumContainsAll(op.getMapType(), mlir::omp::ClauseMapFlags::ref_ptee); mlir::ArrayAttr newMembersAttr; mlir::SmallVector newMembers; llvm::SmallVector> memberIndices; if (!mapMemberUsers.empty() || !op.getMembers().empty()) getMemberIndicesAsVectors( !mapMemberUsers.empty() ? mapMemberUsers[0].parent : op, memberIndices); // For has_device_address we currently do not emit the base address // or an attach map. mlir::omp::MapInfoOp baseAddr; if (!isHasDeviceAddrFlag) { baseAddr = genBaseAddrMap(op.getLoc(), descriptor, op, op.getMapType(), builder, /*IsRefPtee=*/false, mapperId); createBaseAddrInsertion(builder, op, baseAddr, mapMemberUsers, newMembersAttr, newMembers, memberIndices); } // If we have been provided RefPtrPtee, utilise the user specified map // types, otherwise, use the default descriptor map types. auto mapType = isRefPtrPtee ? op.getMapType() : getDescriptorMapType(op.getMapType(), target); mapType = removeAttachModifiers(mapType); auto newMapInfoOp = mlir::omp::MapInfoOp::create( builder, op->getLoc(), op.getResult().getType(), descriptor, mlir::TypeAttr::get(fir::unwrapRefType(descriptor.getType())), builder.getAttr(mapType), op.getMapCaptureTypeAttr(), /*varPtrPtr=*/mlir::Value{}, /*varPtrPtTyper=*/mlir::TypeAttr{}, newMembers, newMembersAttr, /*bounds=*/mlir::SmallVector{}, /*mapperId*/ mlir::FlatSymbolRefAttr(), op.getNameAttr(), /*partial_map=*/builder.getBoolAttr(false)); mlir::Operation *attachMap = nullptr; if (!isAttachNever && !isHasDeviceAddrFlag) attachMap = genImplicitAttachMap(op, descriptor, mapMemberUsers, target, builder, mlir::omp::ClauseMapFlags::ref_ptr | mlir::omp::ClauseMapFlags::ref_ptee, isAttachAlways, baseAddr.getVarPtrPtr()); op.replaceAllUsesWith(newMapInfoOp.getResult()); op->erase(); // The deferral only applies to cases where we map both the descriptor and // base address at once, and when provided ref_ptr_ptee by a user we // assume they know what they're asking for and don't intervene. if (descCanBeDeferred && !isRefPtrPtee) deferrableDesc.push_back(std::make_pair(newMapInfoOp, attachMap)); } // This function handles the splitting of allocatable/pointer maps in // Fortran into descriptor, pointer and attach map components, as // well as the handling of ref_ptr, ref_ptee, ref_ptr_ptee and attach // modifier semantics. // // It delegates to the appropriate helper function based on the mapping type: // - genRefPtrMap: for ref_ptr mappings without members // - genRefPteeMap: for ref_ptee mappings // - genRefPtrPteeOrDefaultMap: for ref_ptr_ptee or default descriptor // mappings void genDescriptorMaps(mlir::omp::MapInfoOp op, fir::FirOpBuilder &builder, mlir::Operation *target) { bool descCanBeDeferred = false; llvm::SmallVector mapMemberUsers; getMemberUserList(op, mapMemberUsers); // TODO: map the addendum segment of the descriptor, similarly to the // base address/data pointer member. bool isHasDeviceAddrFlag = isHasDeviceAddr(op, *target); bool isAttachNever = bitEnumContainsAll( op.getMapType(), mlir::omp::ClauseMapFlags::attach_never); bool isAttachAlways = bitEnumContainsAll( op.getMapType(), mlir::omp::ClauseMapFlags::attach_always); bool isRefPtr = bitEnumContainsAll(op.getMapType(), mlir::omp::ClauseMapFlags::ref_ptr) && !bitEnumContainsAll(op.getMapType(), mlir::omp::ClauseMapFlags::ref_ptee); bool isRefPtee = bitEnumContainsAll(op.getMapType(), mlir::omp::ClauseMapFlags::ref_ptee) && !bitEnumContainsAll(op.getMapType(), mlir::omp::ClauseMapFlags::ref_ptr); mlir::Value descriptor = getDescriptorFromBoxMap(op, builder, descCanBeDeferred); mlir::FlatSymbolRefAttr mapperId = op.getMapperIdAttr(); // If we're a derived type descriptor, that's been flagged as ref_ptr, // but, in the same mapping, we also have members with their own // descriptors also mapped as ref_ptr, then we have to map the parent // derived type descriptors data. This is because the member's ref_ptrs // are parts of the parent and must be mapped and attached as a contiguous // storage block. Relevant for mappings like: // // !$omp target enter data map(ref_ptr, to: obj, obj%arr, // obj%dtype_nest2%arr3, obj%dtype_nest2%scalar_ptr) // // In which a user has basically told us to map the descriptor of obj, but // also bits of its ref_ptee data with its descriptor members. // // TODO: This currently only works for the first level of a // derived-type descriptor chain and will likely need to be extended for the // case where we do a similar style of mapping for deeper nestings. if (isRefPtr && op.getMembers().empty()) { genRefPtrMap(op, builder, target, descriptor, mapMemberUsers, isAttachNever, isAttachAlways); } else if (isRefPtee) { genRefPteeMap(op, builder, target, descriptor, mapMemberUsers, isAttachNever, isAttachAlways, mapperId); } else { genRefPtrPteeOrDefaultMap( op, builder, target, descriptor, mapMemberUsers, isAttachNever, isAttachAlways, isHasDeviceAddrFlag, descCanBeDeferred, mapperId); } } void addImplicitDescriptorMapToTargetDataOp(mlir::omp::MapInfoOp op, fir::FirOpBuilder &builder, mlir::Operation &target) { // Checks if the map is present as an explicit map already on the target // data directive, and not just present on a use_device_addr/ptr, as if // that's the case, we should not need to add an implicit map for the // descriptor. // TODO: We might have to add an implicit attach map as well in these // cases. auto explicitMappingPresent = [](mlir::omp::MapInfoOp op, mlir::omp::TargetDataOp tarData) { // Verify top-level descriptor mapping is at least equal with same // varPtr, the map type should always be To for a descriptor, which is // all we really care about for this mapping as we aim to make sure the // descriptor is always present on device if we're expecting to access // the underlying data. if (tarData.getMapVars().empty()) return false; for (mlir::Value mapVar : tarData.getMapVars()) { auto mapOp = llvm::cast(mapVar.getDefiningOp()); if (mapOp.getVarPtr() == op.getVarPtr() && mapOp.getVarPtrPtr() == op.getVarPtrPtr()) { return true; } } return false; }; // if we're not a top level descriptor with members (e.g. member of a // derived type), we do not want to perform this step. if (!llvm::isa(target) || op.getMembers().empty()) return; if (!isUseDeviceAddr(op, target) && !isUseDevicePtr(op, target)) return; auto targetDataOp = llvm::cast(target); if (explicitMappingPresent(op, targetDataOp)) return; mlir::omp::MapInfoOp newDescParentMapOp = mlir::omp::MapInfoOp::create( builder, op->getLoc(), op.getResult().getType(), op.getVarPtr(), op.getVarPtrTypeAttr(), builder.getAttr( mlir::omp::ClauseMapFlags::to | mlir::omp::ClauseMapFlags::always | mlir::omp::ClauseMapFlags::descriptor), op.getMapCaptureTypeAttr(), /*varPtrPtr=*/mlir::Value{}, /*varPtrPtrType=*/mlir::TypeAttr{}, mlir::SmallVector{}, mlir::ArrayAttr{}, /*bounds=*/mlir::SmallVector{}, /*mapperId*/ mlir::FlatSymbolRefAttr(), op.getNameAttr(), /*partial_map=*/builder.getBoolAttr(false)); llvm::SmallVector parentAndPlacements; targetDataOp.getMapVarsMutable().append({newDescParentMapOp}); genImplicitAttachMap(newDescParentMapOp, op.getVarPtr(), parentAndPlacements, &target, builder, mlir::omp::ClauseMapFlags::ref_ptr, false); } void removeTopLevelDescriptor( std::pair descriptorAndAttach, fir::FirOpBuilder &builder, mlir::Operation *target) { if (llvm::isa(target)) return; // Helper to remove a map from the target's operand list using LLVM // range algorithms. auto eraseFromMapVars = [&](mlir::Value mapResult) { if (auto mapClauseOwner = llvm::dyn_cast(target)) { mlir::MutableOperandRange mapVarsArr = mapClauseOwner.getMapVarsMutable(); auto range = llvm::enumerate(mapVarsArr.getAsOperandRange()); auto it = llvm::find_if( range, [&](auto pair) { return pair.value() == mapResult; }); if (it != range.end()) mapVarsArr.erase((*it).index()); } }; auto mapOp = llvm::dyn_cast(std::get<0>(descriptorAndAttach)); // if we're not a top level descriptor with members (e.g. member of a // derived type), we do not want to perform this step. if (mapOp.getMembers().empty()) return; mlir::SmallVector members = mapOp.getMembers(); mlir::omp::MapInfoOp baseAddr = mlir::dyn_cast_or_null( members.front().getDefiningOp()); assert(baseAddr && "Expected member to be MapInfoOp"); members.erase(members.begin()); llvm::SmallVector> memberIndices; getMemberIndicesAsVectors(mapOp, memberIndices); // Can skip the extra processing if there's only 1 member as it'd // be the base addresses, which we're promoting to the parent. mlir::ArrayAttr membersAttr; if (memberIndices.size() > 1) { memberIndices.erase(memberIndices.begin()); membersAttr = builder.create2DI64ArrayAttr(memberIndices); } // Below we are generating a new base address map and dropping the // descriptor map that we previously had. To do this we generate a load, if // necessary, and then generate a new map utilizing a mixture of the old // descriptor maps arguments and the base address's map arguments. For // example, we basically shift the old varPtrPtr field to varPtr, carry over // the members and their indices from the descriptor and utilise the bounds // from the old base address. auto loadBaseAddr = builder.loadIfRef(mapOp->getLoc(), baseAddr.getVarPtrPtr()); mlir::omp::MapInfoOp newBaseAddrMapOp = mlir::omp::MapInfoOp::create( builder, mapOp->getLoc(), loadBaseAddr.getType(), loadBaseAddr, baseAddr.getVarPtrPtrTypeAttr(), baseAddr.getMapTypeAttr(), baseAddr.getMapCaptureTypeAttr(), /*varPtrPtr=*/mlir::Value{}, /*varPtrPtrType=*/mlir::TypeAttr{}, members, membersAttr, baseAddr.getBounds(), /*mapperId*/ mlir::FlatSymbolRefAttr(), mapOp.getNameAttr(), /*partial_map=*/builder.getBoolAttr(false)); mapOp.replaceAllUsesWith(newBaseAddrMapOp.getResult()); mapOp->erase(); // As we have replaced the old base address with the new base address above // and rewrote all of the descriptor uses with it, we now must make sure we // fully get ride of the base address map to prevent issues with later // processing. eraseFromMapVars(baseAddr.getResult()); baseAddr.erase(); // Also erasing related attach maps for now as they should be unrequired // in these cases. We should, in theory, be able to attach at the target // sites when descriptor and data are present. auto attachMapOp = llvm::dyn_cast_or_null( std::get<1>(descriptorAndAttach)); if (attachMapOp) { eraseFromMapVars(attachMapOp.getResult()); attachMapOp->dropAllUses(); attachMapOp->erase(); } } static bool hasADescriptor(mlir::Operation *varOp, mlir::Type varType) { if (fir::isTypeWithDescriptor(varType) || mlir::isa(varType) || mlir::isa_and_present(varOp)) return true; return false; } // This pass executes on omp::MapInfoOp's containing descriptor based types // (allocatables, pointers, assumed shape etc.) and expanding them into // multiple omp::MapInfoOp's for each pointer member contained within the // descriptor. // // From the perspective of the MLIR pass manager this runs on the top level // operation (usually function) containing the MapInfoOp because this pass // will mutate siblings of MapInfoOp. void runOnOperation() override { mlir::ModuleOp module = mlir::cast(getOperation()); fir::KindMapping kindMap = fir::getKindMapping(module); fir::FirOpBuilder builder{module, std::move(kindMap)}; // We wish to maintain some function level scope (currently // just local function scope variables used to load and store box // variables into so we can access their base address, an // quirk of box_offset requires us to have an in memory box, but Fortran // in certain cases does not provide this) whilst not subjecting // ourselves to the possibility of race conditions while this pass // undergoes frequent re-iteration for the near future. So we loop // over function in the module and then map.info inside of those. getOperation()->walk([&](mlir::Operation *func) { if (!mlir::isa(func)) return; // clear all local allocations we made for any boxes in any prior // iterations from previous function scopes. localBoxAllocas.clear(); deferrableDesc.clear(); expandedBaseAddr.clear(); // Walk all of the existing maps for parents with child maps and then // make sure to appropriately bind them to the target region that the // parent is bound to. Necessary for the next implicit record member // map step which depends on this canonicalization step. This step // is executed again as the final step of this pass to maintain // map to block argument consistency. func->walk([&](mlir::omp::MapInfoOp op) { mlir::Operation *targetUser = getFirstTargetUser(op); assert(targetUser && "expected user of map operation was not found"); addImplicitMembersToTarget(op, builder, targetUser); }); // Next, walk `omp.map.info` ops to see if any record members should be // implicitly mapped. // TODO/FIXME/UPDATE: I believe we need to add implicit capture of // allocatable members of arbitrary depths for this before we can // switch it on in ATD, as currently it will break some currently // downstream changes that existing working benchmarks depend on. // However, hopefully with the addition of: // https://github.com/llvm/llvm-project/pull/119588 // and the correct mapping of all allocatable members, we'd // get the desired behaviour in all cases, if not, need to have a // think about the current behaviour we have. func->walk([&](mlir::omp::MapInfoOp op) { mlir::Type underlyingType = fir::unwrapRefType(op.getVarPtr().getType()); // Test with and support records (derived types) that have allocatable // members directly or nested via other records. if (!fir::isRecordWithAllocatableMember(underlyingType)) return; // For now, only consider `omp.target` ops. Other ops that support // `map` clauses will follow later. mlir::omp::TargetOp target = mlir::dyn_cast_if_present( getFirstTargetUser(op)); if (!target) return; auto mapClauseOwner = llvm::dyn_cast(*target); int64_t mapVarIdx = mapClauseOwner.getOperandIndexForMap(op); assert(mapVarIdx >= 0 && mapVarIdx < static_cast(mapClauseOwner.getMapVars().size())); auto argIface = llvm::dyn_cast(*target); // TODO How should `map` block argument that correspond to: `private`, // `use_device_addr`, `use_device_ptr`, be handled? mlir::BlockArgument opBlockArg = argIface.getMapBlockArgs()[mapVarIdx]; llvm::SetVector mapVarForwardSlice; mlir::getForwardSlice(opBlockArg, &mapVarForwardSlice); mapVarForwardSlice.remove_if([&](mlir::Operation *sliceOp) { // TODO Support coordinate_of ops and calls (by tracking parameters). return !mlir::isa(sliceOp); }); auto recordType = mlir::cast(underlyingType); llvm::SmallVector newMapOpsForFields; llvm::SmallVector> newMemberIndexPaths; // 1) Handle direct top-level allocatable fields. for (auto fieldMemTyPair : recordType.getTypeList()) { auto &field = fieldMemTyPair.first; auto memTy = fieldMemTyPair.second; if (!fir::isAllocatableType(memTy)) continue; bool referenced = llvm::any_of(mapVarForwardSlice, [&](auto *opv) { auto designateOp = mlir::dyn_cast(opv); return designateOp && designateOp.getComponent() && designateOp.getComponent()->strref() == field; }); if (!referenced) continue; int32_t fieldIdx = recordType.getFieldIndex(field); builder.setInsertionPoint(op); fir::IntOrValue idxConst = mlir::IntegerAttr::get(builder.getI32Type(), fieldIdx); auto fieldCoord = fir::CoordinateOp::create( builder, op.getLoc(), builder.getRefType(memTy), op.getVarPtr(), llvm::SmallVector{idxConst}); int64_t fieldIdx64 = static_cast(fieldIdx); llvm::SmallVector idxPath{fieldIdx64}; appendMemberMapIfNew(op, builder, op.getLoc(), fieldCoord, idxPath, field, newMapOpsForFields, newMemberIndexPaths); } // 2) Handle nested allocatable fields along any component chain // referenced in the region via HLFIR designates. llvm::SmallVector> seenIndexPaths; for (mlir::Operation *sliceOp : mapVarForwardSlice) { auto designateOp = mlir::dyn_cast(sliceOp); if (!designateOp || !designateOp.getComponent()) continue; llvm::SmallVector compPathReversed; compPathReversed.push_back(designateOp.getComponent()->strref()); mlir::Value curBase = designateOp.getMemref(); bool rootedAtMapArg = false; while (true) { if (auto parentDes = curBase.getDefiningOp()) { if (!parentDes.getComponent()) break; compPathReversed.push_back(parentDes.getComponent()->strref()); curBase = parentDes.getMemref(); continue; } if (auto decl = curBase.getDefiningOp()) { if (auto barg = mlir::dyn_cast(decl.getMemref())) rootedAtMapArg = (barg == opBlockArg); } else if (auto blockArg = mlir::dyn_cast_or_null( curBase)) { rootedAtMapArg = (blockArg == opBlockArg); } break; } // Only process nested paths (2+ components). Single-component paths // for direct fields are handled above. if (!rootedAtMapArg || compPathReversed.size() < 2) continue; builder.setInsertionPoint(op); llvm::SmallVector indexPath; mlir::Type curTy = underlyingType; mlir::Value coordRef = op.getVarPtr(); bool validPath = true; for (llvm::StringRef compName : llvm::reverse(compPathReversed)) { auto recTy = mlir::dyn_cast(curTy); if (!recTy) { validPath = false; break; } int32_t idx = recTy.getFieldIndex(compName); if (idx < 0) { validPath = false; break; } indexPath.push_back(idx); mlir::Type memTy = recTy.getType(idx); fir::IntOrValue idxConst = mlir::IntegerAttr::get(builder.getI32Type(), idx); coordRef = fir::CoordinateOp::create( builder, op.getLoc(), builder.getRefType(memTy), coordRef, llvm::SmallVector{idxConst}); curTy = memTy; } if (!validPath) continue; if (auto finalRefTy = mlir::dyn_cast(coordRef.getType())) { mlir::Type eleTy = finalRefTy.getElementType(); if (fir::isAllocatableType(eleTy)) { if (!containsPath(seenIndexPaths, indexPath)) { seenIndexPaths.emplace_back(indexPath.begin(), indexPath.end()); appendMemberMapIfNew(op, builder, op.getLoc(), coordRef, indexPath, compPathReversed.front(), newMapOpsForFields, newMemberIndexPaths); } } } } if (newMapOpsForFields.empty()) return; // Deduplicate by index path to avoid emitting duplicate members for // the same component. Use a set-based key to keep this near O(n). llvm::SmallVector dedupMapOps; llvm::SmallVector> dedupIndexPaths; llvm::StringSet<> seenKeys; for (auto [i, mapOpV] : llvm::enumerate(newMapOpsForFields)) { const auto &path = newMemberIndexPaths[i]; llvm::SmallString<64> key; buildPathKey(path, key); if (seenKeys.contains(key)) continue; seenKeys.insert(key); dedupMapOps.push_back(mapOpV); dedupIndexPaths.emplace_back(path.begin(), path.end()); } op.getMembersMutable().append(dedupMapOps); llvm::SmallVector> newMemberIndices; if (mlir::ArrayAttr oldAttr = op.getMembersIndexAttr()) for (mlir::Attribute indexList : oldAttr) { llvm::SmallVector listVec; for (mlir::Attribute index : mlir::cast(indexList)) listVec.push_back(mlir::cast(index).getInt()); newMemberIndices.emplace_back(std::move(listVec)); } for (auto &path : dedupIndexPaths) newMemberIndices.emplace_back(path); op.setMembersIndexAttr(builder.create2DI64ArrayAttr(newMemberIndices)); // Set to partial map only if there is no user-defined mapper. op.setPartialMap(op.getMapperIdAttr() == nullptr); }); // Expand type(C_PTR) only when unified_shared_memory is required, // to ensure device-visible pointer size/behavior in USM scenarios // without changing default expectations elsewhere. func->walk([&](mlir::omp::MapInfoOp op) { // Only expand C_PTR members when unified_shared_memory is required. if (!moduleRequiresUSM(func->getParentOfType())) return; builder.setInsertionPoint(op); genCptrMemberMap(op, builder); }); func->walk([&](mlir::omp::MapInfoOp op) { // NOTE: Currently only supports a single user for the MapInfoOp. This // is fine for the moment, as the Fortran frontend will generate a // new MapInfoOp with at most one user currently. In the case of // members of other objects, like derived types, the user would be the // parent. In cases where it's a regular non-member map, the user would // be the target operation it is being mapped by. // // However, when/if we optimise/cleanup the IR we will have to extend // this pass to support multiple users, as we may wish to have a map // be re-used by multiple users (e.g. across multiple targets that map // the variable and have identical map properties). assert(verifyUsesConstraint(op) && "OMPMapInfoFinalization currently only supports " "single users or up to two users when those users" "are a MapInfoOp and Target mapping directive"); if (hasADescriptor(op.getVarPtr().getDefiningOp(), fir::unwrapRefType(op.getVarPtrType()))) { builder.setInsertionPoint(op); mlir::Operation *targetUser = getFirstTargetUser(op); assert(targetUser && "expected user of map operation was not found"); genDescriptorMaps(op, builder, targetUser); } }); func->walk([&](mlir::omp::MapInfoOp op) { // If a record type is not mapped with the `close` modifier while some // of its members are (e.g. descriptor maps), then in USM mode, the // memory for the record will be allocated in unified memory while the // the members might be allocated in device memory. This creates an // inconsistent map for the record type where some of its members are // allocated in different address spaces. // // This fixes this issue by taking a conservative approach and removing // the `close` flag from members if it is not used for mapping the // parent record. if (op.getMembers().empty()) return; mlir::Type varTy = fir::unwrapRefType(op.getVarPtr().getType()); if (!mlir::isa(varTy)) return; auto mapFlag = op.getMapType(); bool hasClose = (mapFlag & mlir::omp::ClauseMapFlags::close) == mlir::omp::ClauseMapFlags::close; if (hasClose) return; for (auto member : op.getMembers()) { if (auto memberOp = llvm::dyn_cast_if_present( member.getDefiningOp())) { auto memberMapFlag = memberOp.getMapType() & ~mlir::omp::ClauseMapFlags::close; memberOp.setMapType(memberMapFlag); } } }); // Now that we've expanded all of our boxes into a descriptor and base // address map where necessary, we check if the map owner is an // enter/exit/target data directive, and if they are we drop the initial // descriptor (top-level parent) and replace it with the // base_address/data. // // This circumvents issues with stack allocated descriptors bound to // device colliding which in Flang is rather trivial for a user to do by // accident due to the rather pervasive local intermediate descriptor // generation that occurs whenever you pass boxes around different scopes. // In OpenMP 6+ mapping these would be a user error as the tools required // to circumvent these issues are provided by the spec (ref_ptr/ptee map // types), but in prior specifications these tools are not available and // it becomes an implementation issue for us to solve. // // We do this by dropping the top-level descriptor which will be the stack // descriptor when we perform enter/exit maps, as we don't want these to // be bound until necessary which is when we utilise the descriptor type // within a target region. At which point we map the relevant descriptor // data and the runtime should correctly associate the data with the // descriptor and bind together and allow clean mapping and execution. if (deferDescMapping) { for (auto deferrableAndAttach : deferrableDesc) { auto mapOp = llvm::dyn_cast( std::get<0>(deferrableAndAttach)); mlir::Operation *targetUser = getFirstTargetUser(mapOp); assert(targetUser && "expected user of map operation was not found"); builder.setInsertionPoint(mapOp); removeTopLevelDescriptor(deferrableAndAttach, builder, targetUser); addImplicitDescriptorMapToTargetDataOp(mapOp, builder, *targetUser); } } // Wait until after we have generated all of our maps to add them onto // the target's block arguments, simplifying the process as there would be // no need to avoid accidental duplicate additions. func->walk([&](mlir::omp::MapInfoOp op) { mlir::Operation *targetUser = getFirstTargetUser(op); assert(targetUser && "expected user of map operation was not found"); addImplicitMembersToTarget(op, builder, targetUser); }); }); } }; } // namespace std::unique_ptr flangomp::createMapInfoFinalizationPass(bool deferDescMap) { MapInfoFinalizationPassOptions options; options.deferDescMapping = deferDescMap; return std::make_unique(options); }