blob: af30aa757a15a218702295a22dd46ab38e20fb12 [file]
//===--------- device.cpp - Target independent OpenMP target RTL ----------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Functionality for managing devices that are handled by RTL plugins.
//
//===----------------------------------------------------------------------===//
#include "device.h"
#include "OffloadEntry.h"
#include "OpenMP/Mapping.h"
#include "OpenMP/OMPT/Callback.h"
#include "OpenMP/OMPT/Interface.h"
#include "PluginManager.h"
#include "Shared/APITypes.h"
#include "Shared/Debug.h"
#include "omptarget.h"
#include "rtl.h"
#include "Shared/EnvironmentVar.h"
#include "llvm/Frontend/OpenMP/OMPConstants.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/MathExtras.h"
#include <algorithm>
#include <cassert>
#include <climits>
#include <cstdint>
#include <cstdio>
#include <limits>
#include <mutex>
#include <string>
#include <thread>
#ifdef OMPT_SUPPORT
using namespace llvm::omp::target::ompt;
#endif
using namespace llvm::omp::target::plugin;
using namespace llvm::omp::target::debug;
int HostDataToTargetTy::addEventIfNecessary(DeviceTy &Device,
AsyncInfoTy &AsyncInfo) const {
// First, check if the user disabled atomic map transfer/malloc/dealloc.
if (!MappingConfig::get().UseEventsForAtomicTransfers)
return OFFLOAD_SUCCESS;
void *Event = getEvent();
bool NeedNewEvent = Event == nullptr;
if (NeedNewEvent && Device.createEvent(&Event) != OFFLOAD_SUCCESS) {
REPORT() << "Failed to create event";
return OFFLOAD_FAIL;
}
// We cannot assume the event should not be nullptr because we don't
// know if the target support event. But if a target doesn't,
// recordEvent should always return success.
if (Device.recordEvent(Event, AsyncInfo) != OFFLOAD_SUCCESS) {
REPORT() << "Failed to set dependence on event " << Event;
return OFFLOAD_FAIL;
}
if (NeedNewEvent)
setEvent(Event);
return OFFLOAD_SUCCESS;
}
DeviceTy::DeviceTy(GenericPluginTy *RTL, int32_t DeviceID, int32_t RTLDeviceID,
ol_device_handle_t DeviceHandle)
: DeviceID(DeviceID), RTL(RTL), RTLDeviceID(RTLDeviceID),
DeviceHandle(DeviceHandle), MappingInfo(*this) {}
DeviceTy::~DeviceTy() {
if (DeviceID == -1 || !(getInfoLevel() & OMP_INFOTYPE_DUMP_TABLE))
return;
ident_t Loc = {0, 0, 0, 0, ";libomptarget;libomptarget;0;0;;"};
dumpTargetPointerMappings(&Loc, *this);
}
llvm::Error DeviceTy::init() {
OMPT_IF_BUILT_AND_INITIALIZED({
GenericDeviceTy &GenericDevice = RTL->getDevice(RTLDeviceID);
std::string ComputeUnitKind = GenericDevice.getComputeUnitKind();
performOmptCallback(device_initialize, DeviceID, ComputeUnitKind.c_str(),
reinterpret_cast<ompt_device_t *>(&GenericDevice),
lookupCallbackByName, /*documentation=*/nullptr);
});
// Enables recording kernels if set.
BoolEnvar OMPX_RecordKernel("LIBOMPTARGET_RECORD", false);
if (OMPX_RecordKernel) {
BoolEnvar OMPX_RecordOutput("LIBOMPTARGET_RECORD_OUTPUT", true);
Int64Envar OMPX_RecordMemSize("LIBOMPTARGET_RECORD_MEMSIZE",
8 * 1024 * 1024 * 1024ULL);
Int32Envar OMPX_RecordDevice("LIBOMPTARGET_RECORD_DEVICE", 0);
StringEnvar OMPX_RecordOutputDir("LIBOMPTARGET_RECORD_DIR", "");
BoolEnvar OMPX_EmitRecordReport("LIBOMPTARGET_RECORD_REPORT", false);
StringEnvar OMPX_RecordReportFilename("LIBOMPTARGET_RECORD_REPORT_FILENAME",
"");
if (OMPX_RecordDevice != RTLDeviceID)
return llvm::Error::success();
// Print report if it was enabled explicitly or a report file was indicated.
bool EmitReport =
OMPX_EmitRecordReport || !OMPX_RecordReportFilename.get().empty();
int32_t Ret = RTL->initialize_record_replay(
RTLDeviceID, OMPX_RecordMemSize, nullptr,
/*IsRecord=*/true, /*IsNative=*/true, OMPX_RecordOutput, EmitReport,
OMPX_RecordReportFilename.get().c_str(),
OMPX_RecordOutputDir.get().c_str());
if (Ret != OFFLOAD_SUCCESS)
return error::createOffloadError(error::ErrorCode::BACKEND_FAILURE,
"failed to initialize RR in device %d\n",
DeviceID);
}
return llvm::Error::success();
}
// Extract the mapping of host function pointers to device function pointers
// from the entry table. Functions marked as 'indirect' in OpenMP will have
// offloading entries generated for them which map the host's function pointer
// to a global containing the corresponding function pointer on the device.
static llvm::Expected<std::pair<void *, uint64_t>>
setupIndirectCallTable(DeviceTy &Device, __tgt_device_image *Image,
__tgt_device_binary Binary) {
AsyncInfoTy AsyncInfo(Device);
llvm::ArrayRef<llvm::offloading::EntryTy> Entries(Image->EntriesBegin,
Image->EntriesEnd);
llvm::SmallVector<std::pair<void *, void *>> IndirectCallTable;
for (const auto &Entry : Entries) {
if (Entry.Kind != llvm::object::OffloadKind::OFK_OpenMP ||
Entry.Size == 0 ||
(!(Entry.Flags & OMP_DECLARE_TARGET_INDIRECT) &&
!(Entry.Flags & OMP_DECLARE_TARGET_INDIRECT_VTABLE)))
continue;
size_t PtrSize = sizeof(void *);
if (Entry.Flags & OMP_DECLARE_TARGET_INDIRECT_VTABLE) {
// This is a VTable entry, the current entry is the first index of the
// VTable and Entry.Size is the total size of the VTable. Unlike the
// indirect function case below, the Global is not of size Entry.Size and
// is instead of size PtrSize (sizeof(void*)).
void *Vtable;
void *res;
if (Device.RTL->get_global(Binary, PtrSize, Entry.SymbolName, &Vtable))
return error::createOffloadError(error::ErrorCode::INVALID_BINARY,
"failed to load %s", Entry.SymbolName);
// HstPtr = Entry.Address;
if (Device.retrieveData(&res, Vtable, PtrSize, AsyncInfo))
return error::createOffloadError(error::ErrorCode::INVALID_BINARY,
"failed to load %s", Entry.SymbolName);
if (Device.synchronize(AsyncInfo))
return error::createOffloadError(
error::ErrorCode::INVALID_BINARY,
"failed to synchronize after retrieving %s", Entry.SymbolName);
// Calculate and emplace entire Vtable from first Vtable byte
for (uint64_t i = 0; i < Entry.Size / PtrSize; ++i) {
auto &[HstPtr, DevPtr] = IndirectCallTable.emplace_back();
HstPtr = reinterpret_cast<void *>(
reinterpret_cast<uintptr_t>(Entry.Address) + i * PtrSize);
DevPtr = reinterpret_cast<void *>(reinterpret_cast<uintptr_t>(res) +
i * PtrSize);
}
} else {
// Indirect function case: Entry.Size should equal PtrSize since we're
// dealing with a single function pointer (not a VTable)
assert(Entry.Size == PtrSize && "Global not a function pointer?");
auto &[HstPtr, DevPtr] = IndirectCallTable.emplace_back();
void *Ptr;
if (Device.RTL->get_global(Binary, Entry.Size, Entry.SymbolName, &Ptr))
return error::createOffloadError(error::ErrorCode::INVALID_BINARY,
"failed to load %s", Entry.SymbolName);
HstPtr = Entry.Address;
if (Device.retrieveData(&DevPtr, Ptr, Entry.Size, AsyncInfo))
return error::createOffloadError(error::ErrorCode::INVALID_BINARY,
"failed to load %s", Entry.SymbolName);
}
if (Device.synchronize(AsyncInfo))
return error::createOffloadError(
error::ErrorCode::INVALID_BINARY,
"failed to synchronize after retrieving %s", Entry.SymbolName);
}
// If we do not have any indirect globals we exit early.
if (IndirectCallTable.empty())
return std::pair{nullptr, 0};
// Sort the array to allow for more efficient lookup of device pointers.
llvm::sort(IndirectCallTable,
[](const auto &x, const auto &y) { return x.first < y.first; });
uint64_t TableSize =
IndirectCallTable.size() * sizeof(std::pair<void *, void *>);
void *DevicePtr = Device.allocData(TableSize, nullptr, TARGET_ALLOC_DEVICE);
if (Device.submitData(DevicePtr, IndirectCallTable.data(), TableSize,
AsyncInfo))
return error::createOffloadError(error::ErrorCode::INVALID_BINARY,
"failed to copy data");
// The IndirectCallTable is on the stack, so we must synchronize to ensure
// the data is copied before we return.
if (Device.synchronize(AsyncInfo))
return error::createOffloadError(
error::ErrorCode::INVALID_BINARY,
"failed to synchronize after copying data");
return std::pair<void *, uint64_t>(DevicePtr, IndirectCallTable.size());
}
// Load binary to device and perform global initialization if needed.
llvm::Expected<__tgt_device_binary>
DeviceTy::loadBinary(__tgt_device_image *Img) {
__tgt_device_binary Binary;
if (RTL->load_binary(RTLDeviceID, Img, &Binary) != OFFLOAD_SUCCESS)
return error::createOffloadError(error::ErrorCode::INVALID_BINARY,
"failed to load binary %p", Img);
OMPT_IF_BUILT_AND_INITIALIZED(performOmptCallback(
device_load, DeviceID, /*FileName=*/nullptr, /*FileOffset=*/0,
/*VmaInFile=*/nullptr,
reinterpret_cast<uintptr_t>(Img->ImageEnd) -
reinterpret_cast<uintptr_t>(Img->ImageStart),
const_cast<void *>(Img->ImageStart),
/*DeviceAddr=*/nullptr, /*ModuleId=*/0));
// This symbol is optional.
void *DeviceEnvironmentPtr;
if (RTL->get_global(Binary, sizeof(DeviceEnvironmentTy),
"__omp_rtl_device_environment", &DeviceEnvironmentPtr))
return Binary;
// Obtain a table mapping host function pointers to device function pointers.
auto CallTablePairOrErr = setupIndirectCallTable(*this, Img, Binary);
if (!CallTablePairOrErr)
return CallTablePairOrErr.takeError();
GenericDeviceTy &GenericDevice = RTL->getDevice(RTLDeviceID);
DeviceEnvironmentTy DeviceEnvironment;
DeviceEnvironment.DeviceDebugKind = GenericDevice.getDebugKind();
DeviceEnvironment.NumDevices = RTL->getNumDevices();
// TODO: The device ID used here is not the real device ID used by OpenMP.
DeviceEnvironment.DeviceNum = RTLDeviceID;
DeviceEnvironment.DynamicMemSize = 0;
DeviceEnvironment.ClockFrequency = GenericDevice.getClockFrequency();
DeviceEnvironment.IndirectCallTable =
reinterpret_cast<uintptr_t>(CallTablePairOrErr->first);
DeviceEnvironment.IndirectCallTableSize = CallTablePairOrErr->second;
DeviceEnvironment.HardwareParallelism =
GenericDevice.getHardwareParallelism();
AsyncInfoTy AsyncInfo(*this);
if (submitData(DeviceEnvironmentPtr, &DeviceEnvironment,
sizeof(DeviceEnvironment), AsyncInfo))
return error::createOffloadError(error::ErrorCode::INVALID_BINARY,
"failed to copy data");
return Binary;
}
void *DeviceTy::allocData(int64_t Size, void *HstPtr, int32_t Kind) {
/// RAII to establish tool anchors before and after data allocation
void *TargetPtr = nullptr;
OMPT_IF_BUILT(InterfaceRAII TargetDataAllocRAII(
RegionInterface.getCallbacks<ompt_target_data_alloc>(),
DeviceID, HstPtr, &TargetPtr, Size,
/*CodePtr=*/OMPT_GET_RETURN_ADDRESS);)
TargetPtr = RTL->data_alloc(RTLDeviceID, Size, HstPtr, Kind);
return TargetPtr;
}
int32_t DeviceTy::deleteData(void *TgtAllocBegin, int32_t Kind) {
/// RAII to establish tool anchors before and after data deletion
OMPT_IF_BUILT(InterfaceRAII TargetDataDeleteRAII(
RegionInterface.getCallbacks<ompt_target_data_delete>(),
DeviceID, TgtAllocBegin,
/*CodePtr=*/OMPT_GET_RETURN_ADDRESS);)
return RTL->data_delete(RTLDeviceID, TgtAllocBegin, Kind);
}
// Submit data to device
int32_t DeviceTy::submitData(void *TgtPtrBegin, void *HstPtrBegin, int64_t Size,
AsyncInfoTy &AsyncInfo, HostDataToTargetTy *Entry,
MappingInfoTy::HDTTMapAccessorTy *HDTTMapPtr) {
if (getInfoLevel() & OMP_INFOTYPE_DATA_TRANSFER)
MappingInfo.printCopyInfo(TgtPtrBegin, HstPtrBegin, Size, /*H2D=*/true,
Entry, HDTTMapPtr);
/// RAII to establish tool anchors before and after data submit
OMPT_IF_BUILT(
InterfaceRAII TargetDataSubmitRAII(
RegionInterface.getCallbacks<ompt_target_data_transfer_to_device>(),
omp_initial_device, HstPtrBegin, DeviceID, TgtPtrBegin, Size,
/*CodePtr=*/OMPT_GET_RETURN_ADDRESS);)
return RTL->data_submit_async(RTLDeviceID, TgtPtrBegin, HstPtrBegin, Size,
AsyncInfo);
}
// Retrieve data from device
int32_t DeviceTy::retrieveData(void *HstPtrBegin, void *TgtPtrBegin,
int64_t Size, AsyncInfoTy &AsyncInfo,
HostDataToTargetTy *Entry,
MappingInfoTy::HDTTMapAccessorTy *HDTTMapPtr) {
if (getInfoLevel() & OMP_INFOTYPE_DATA_TRANSFER)
MappingInfo.printCopyInfo(TgtPtrBegin, HstPtrBegin, Size, /*H2D=*/false,
Entry, HDTTMapPtr);
/// RAII to establish tool anchors before and after data retrieval
OMPT_IF_BUILT(
InterfaceRAII TargetDataRetrieveRAII(
RegionInterface.getCallbacks<ompt_target_data_transfer_from_device>(),
DeviceID, TgtPtrBegin, omp_initial_device, HstPtrBegin, Size,
/*CodePtr=*/OMPT_GET_RETURN_ADDRESS);)
return RTL->data_retrieve_async(RTLDeviceID, HstPtrBegin, TgtPtrBegin, Size,
AsyncInfo);
}
// Copy data from current device to destination device directly
int32_t DeviceTy::dataExchange(void *SrcPtr, DeviceTy &DstDev, void *DstPtr,
int64_t Size, AsyncInfoTy &AsyncInfo) {
/// RAII to establish tool anchors before and after data exchange
/// Note: Despite the fact that this is a data exchange, we use 'from_device'
/// operation enum (w.r.t. ompt_target_data_op_t) as there is currently
/// no better alternative. It is still possible to distinguish this
/// scenario from a real data retrieve by checking if both involved
/// device numbers are less than omp_get_num_devices().
OMPT_IF_BUILT(
InterfaceRAII TargetDataExchangeRAII(
RegionInterface.getCallbacks<ompt_target_data_transfer_from_device>(),
RTLDeviceID, SrcPtr, DstDev.RTLDeviceID, DstPtr, Size,
/*CodePtr=*/OMPT_GET_RETURN_ADDRESS);)
if (!AsyncInfo) {
return RTL->data_exchange(RTLDeviceID, SrcPtr, DstDev.RTLDeviceID, DstPtr,
Size);
}
return RTL->data_exchange_async(RTLDeviceID, SrcPtr, DstDev.RTLDeviceID,
DstPtr, Size, AsyncInfo);
}
int32_t DeviceTy::dataFence(AsyncInfoTy &AsyncInfo) {
return RTL->data_fence(RTLDeviceID, AsyncInfo);
}
int32_t DeviceTy::notifyDataMapped(void *HstPtr, int64_t Size) {
ODBG(ODT_Mapping) << "Notifying about new mapping: HstPtr=" << HstPtr
<< ", Size=" << Size;
if (RTL->data_notify_mapped(RTLDeviceID, HstPtr, Size)) {
REPORT() << "Notifying about data mapping failed.";
return OFFLOAD_FAIL;
}
return OFFLOAD_SUCCESS;
}
int32_t DeviceTy::notifyDataUnmapped(void *HstPtr) {
ODBG(ODT_Mapping) << "Notifying about an unmapping: HstPtr=" << HstPtr;
if (RTL->data_notify_unmapped(RTLDeviceID, HstPtr)) {
REPORT() << "Notifying about data unmapping failed.";
return OFFLOAD_FAIL;
}
return OFFLOAD_SUCCESS;
}
/// Resolve \p NumArgs (base pointer, offset) pairs into a flattened array of
/// argument-value pointers suitable for a kernel launch, writing the result
/// into \p LaunchArgs.NumArgs/Args.
static void resolveKernelLaunchParams(void **const TgtArgs,
ptrdiff_t *const TgtOffsets,
uint32_t NumArgs,
llvm::SmallVector<void *> &Args,
llvm::SmallVector<void *> &Ptrs,
KernelLaunchArgsTy &LaunchArgs) {
LaunchArgs.NumArgs = NumArgs;
Args.resize(NumArgs);
Ptrs.resize(NumArgs);
if (NumArgs == 0)
return;
for (uint32_t I = 0; I < NumArgs; ++I) {
Args[I] = reinterpret_cast<void *>(reinterpret_cast<intptr_t>(TgtArgs[I]) +
TgtOffsets[I]);
Ptrs[I] = &Args[I];
}
LaunchArgs.Args = &Ptrs[0];
}
namespace {
/// Configuration of dynamic block memory needed for launching a kernel.
struct DynBlockMemConfTy {
/// The size of the dynamic block memory buffer.
uint32_t Size = 0;
/// The size of dynamic shared memory natively provided by the device.
uint32_t NativeSize = 0;
/// The fallback that was triggered (if any).
DynCGroupMemFallbackType Fallback = DynCGroupMemFallbackType::None;
/// The fallback pointer if global memory was used as alternative.
void *FallbackPtr = nullptr;
};
} // namespace
/// Prepare the block memory buffer requested for the kernel and execute the
/// specified fallback if necessary.
static llvm::Expected<DynBlockMemConfTy>
prepareBlockMemory(GenericDeviceTy &GenericDevice,
const KernelLaunchInfoTy &KernelEnv, uint32_t DynCGroupMem,
DynCGroupMemFallbackType DynCGroupMemFallback,
uint32_t NumBlocks) {
// If the fallback is abort, don't try to adjust the block memory size.
if (DynCGroupMemFallback == DynCGroupMemFallbackType::Abort)
return DynBlockMemConfTy{DynCGroupMem, DynCGroupMem,
DynCGroupMemFallbackType::Abort, nullptr};
uint32_t MaxBlockMemSize = GenericDevice.getMaxBlockSharedMemSize();
uint32_t DynBlockMemSize = DynCGroupMem;
uint32_t TotalBlockMemSize = KernelEnv.StaticBlockMemSize + DynBlockMemSize;
uint32_t DynNativeBlockMemSize = DynBlockMemSize;
void *DynFallbackPtr = nullptr;
DynCGroupMemFallbackType DynFallback = DynCGroupMemFallbackType::None;
if (DynBlockMemSize && TotalBlockMemSize > MaxBlockMemSize) {
// Launch without native dynamic block memory.
DynNativeBlockMemSize = 0;
DynFallback = DynCGroupMemFallback;
if (DynFallback != DynCGroupMemFallbackType::DefaultMem) {
// Do not provide any memory as fallback.
DynBlockMemSize = 0;
} else {
// Get global memory as fallback.
auto AllocOrErr = GenericDevice.dataAlloc(
NumBlocks * DynBlockMemSize,
/*HostPtr=*/nullptr, TARGET_ALLOC_DEVICE, /*Alignment=*/0);
if (!AllocOrErr)
return AllocOrErr.takeError();
DynFallbackPtr = *AllocOrErr;
}
}
return DynBlockMemConfTy{DynBlockMemSize, DynNativeBlockMemSize, DynFallback,
DynFallbackPtr};
}
static void freeAfterSynchronization(GenericDeviceTy &GenericDevice,
AsyncInfoTy &AsyncInfo, void *Ptr,
TargetAllocTy Kind) {
AsyncInfo.addPostProcessingFunction([&GenericDevice, Ptr, Kind]() -> int {
if (auto Err = GenericDevice.dataDelete(Ptr, Kind)) {
REPORT() << "Failure to free device memory " << Ptr << ": "
<< toString(std::move(Err));
return OFFLOAD_FAIL;
}
return OFFLOAD_SUCCESS;
});
}
/// Return a device pointer to a new kernel launch environment, or null if
/// this launch has no reserved dyn_ptr slot to store one in. \p NumBlocks0 is
/// the number of blocks for this launch and is used to size the reduction
/// buffer.
static llvm::Expected<KernelLaunchEnvironmentTy *> getKernelLaunchEnvironment(
GenericDeviceTy &GenericDevice, const KernelLaunchArgsTy &LaunchArgs,
const KernelLaunchInfoTy &KernelEnv,
const DynBlockMemConfTy &DynBlockMemConf, uint32_t DynCGroupMem,
void **DynPtrSlot, AsyncInfoTy &AsyncInfo, uint32_t NumBlocks0) {
// Ctor/Dtor have no arguments, replaying uses the original kernel launch
// environment, and launches with no reserved dyn_ptr slot (e.g. older
// compiler versions, or non-OpenMP launches) have nowhere to store one.
if ((GenericDevice.getRecordReplay() &&
GenericDevice.getRecordReplay()->isReplaying()) ||
!DynPtrSlot)
return nullptr;
const bool NeedsReductionBuffer = KernelEnv.ReductionDataSize != 0;
if (NeedsReductionBuffer && LaunchArgs.OmpABIVersion < OMP_KERNEL_ARG_VERSION)
return error::createOffloadError(
error::ErrorCode::INVALID_BINARY,
"kernel was built against an older OpenMP kernel-launch-environment "
"ABI (v%u); current runtime requires v%u for cross-team reductions",
LaunchArgs.OmpABIVersion, OMP_KERNEL_ARG_VERSION);
if (!NeedsReductionBuffer && !DynCGroupMem)
return reinterpret_cast<KernelLaunchEnvironmentTy *>(~0);
auto AllocOrErr = GenericDevice.dataAlloc(
sizeof(KernelLaunchEnvironmentTy),
/*HostPtr=*/nullptr, TARGET_ALLOC_DEVICE, /*Alignment=*/0);
if (!AllocOrErr)
return AllocOrErr.takeError();
// Remember to free the memory later.
freeAfterSynchronization(GenericDevice, AsyncInfo, *AllocOrErr,
TARGET_ALLOC_DEVICE);
// Use the KLE in the __tgt_async_info to ensure a stable address for the
// async data transfer.
auto &LocalKLE =
static_cast<__tgt_async_info *>(AsyncInfo)->KernelLaunchEnvironment;
LocalKLE = KernelLaunchEnvironmentTy{};
LocalKLE.DynCGroupMemSize = DynBlockMemConf.Size;
LocalKLE.DynCGroupMemFbPtr = DynBlockMemConf.FallbackPtr;
LocalKLE.DynCGroupMemFb = DynBlockMemConf.Fallback;
LocalKLE.ReductionBuffer = nullptr;
if (NeedsReductionBuffer) {
// Use number of teams many buffer elements.
auto ReductionAllocOrErr = GenericDevice.dataAlloc(
uint64_t(KernelEnv.ReductionDataSize) * NumBlocks0,
/*HostPtr=*/nullptr, TARGET_ALLOC_DEVICE, /*Alignment=*/0);
if (!ReductionAllocOrErr)
return ReductionAllocOrErr.takeError();
LocalKLE.ReductionBuffer = *ReductionAllocOrErr;
// Remember to free the memory later.
freeAfterSynchronization(GenericDevice, AsyncInfo, *ReductionAllocOrErr,
TARGET_ALLOC_DEVICE);
}
INFO(OMP_INFOTYPE_DATA_TRANSFER, GenericDevice.getDeviceId(),
"Copying data from host to device, HstPtr=" DPxMOD ", TgtPtr=" DPxMOD
", Size=%" PRId64 ", Name=KernelLaunchEnv\n",
DPxPTR(&LocalKLE), DPxPTR(*AllocOrErr),
sizeof(KernelLaunchEnvironmentTy));
if (auto Err = GenericDevice.dataSubmit(
*AllocOrErr, &LocalKLE, sizeof(KernelLaunchEnvironmentTy), AsyncInfo))
return Err;
return static_cast<KernelLaunchEnvironmentTy *>(*AllocOrErr);
}
/// Get the effective number of threads for the kernel based on the
/// user-defined number of threads.
static uint32_t getEffectiveNumThreads(GenericDeviceTy &GenericDevice,
uint32_t UserThreadLimit,
const KernelLaunchInfoTy &KernelEnv) {
assert(!KernelEnv.isBareMode() &&
"bare kernel should not call this function");
if (UserThreadLimit > 0 && KernelEnv.isGenericMode())
UserThreadLimit += GenericDevice.getWarpSize();
return std::min(KernelEnv.MaxNumThreads, (UserThreadLimit > 0)
? UserThreadLimit
: KernelEnv.PreferredNumThreads);
}
/// Get the effective number of blocks for the kernel based on the
/// user-defined number of blocks and the loop trip count.
/// The number of threads \p EffectiveNumThreads can be adjusted by this
/// method. \p IsNumThreadsFromUser is true if \p EffectiveNumThreads is
/// defined by the user via the thread_limit clause.
static uint32_t
getEffectiveNumBlocks(GenericDeviceTy &GenericDevice, uint32_t UserNumBlocks,
uint64_t LoopTripCount, uint32_t &EffectiveNumThreads,
bool IsNumThreadsStrict, bool IsNumThreadsFromUser,
const KernelLaunchInfoTy &KernelEnv) {
assert(!KernelEnv.isBareMode() &&
"bare kernel should not call this function");
// NOTE: This clamps the user-requested number of blocks to the device limit
// rather than honoring it exactly, which is non-standard behavior. Truly
// honoring an arbitrary value would require launching multiple kernels or
// reusing blocks until the requested count has been served.
if (UserNumBlocks > 0)
return std::min(UserNumBlocks,
GenericDevice.getBlockLimit(EffectiveNumThreads));
// Return the number of blocks required to cover the loop iterations.
if (KernelEnv.isNoLoopMode())
return LoopTripCount > 0 ? (((LoopTripCount - 1) / EffectiveNumThreads) + 1)
: 1;
uint64_t DefaultNumBlocks = GenericDevice.getDefaultNumBlocks();
uint64_t TripCountNumBlocks = std::numeric_limits<uint64_t>::max();
if (LoopTripCount > 0) {
if (KernelEnv.isSPMDMode()) {
// We have a combined construct, i.e. `target teams distribute
// parallel for [simd]`. We launch so many blocks so that each thread
// will execute one iteration of the loop; rounded up to the nearest
// integer. However, if that results in too few blocks, we artificially
// reduce the thread count per block to increase the outer parallelism.
auto MinThreads = GenericDevice.getMinThreadsForLowTripCountLoop();
MinThreads = std::min(MinThreads, EffectiveNumThreads);
// Honor the thread_limit clause; only lower the number of threads.
[[maybe_unused]] auto OldNumThreads = EffectiveNumThreads;
if (LoopTripCount >= DefaultNumBlocks * EffectiveNumThreads ||
IsNumThreadsFromUser || IsNumThreadsStrict) {
// Enough parallelism for blocks and threads.
TripCountNumBlocks = ((LoopTripCount - 1) / EffectiveNumThreads) + 1;
assert(IsNumThreadsFromUser ||
TripCountNumBlocks >= DefaultNumBlocks &&
"Expected sufficient outer parallelism.");
} else if (LoopTripCount >= DefaultNumBlocks * MinThreads) {
// Enough parallelism for blocks, limit threads.
// This case is hard; for now, we force "full warps":
// First, compute a thread count assuming DefaultNumBlocks.
auto NumThreadsDefaultBlocks =
(LoopTripCount + DefaultNumBlocks - 1) / DefaultNumBlocks;
// Now get a power of two that is larger or equal.
auto NumThreadsDefaultBlocksP2 =
llvm::PowerOf2Ceil(NumThreadsDefaultBlocks);
// Do not increase a thread limit given be the user.
EffectiveNumThreads =
std::min(EffectiveNumThreads, uint32_t(NumThreadsDefaultBlocksP2));
assert(EffectiveNumThreads >= MinThreads &&
"Expected sufficient inner parallelism.");
TripCountNumBlocks = ((LoopTripCount - 1) / EffectiveNumThreads) + 1;
} else {
// Not enough parallelism for blocks and threads, limit both.
EffectiveNumThreads = std::min(EffectiveNumThreads, MinThreads);
TripCountNumBlocks = ((LoopTripCount - 1) / EffectiveNumThreads) + 1;
}
assert(EffectiveNumThreads * TripCountNumBlocks >= LoopTripCount &&
"Expected sufficient parallelism");
assert(OldNumThreads >= EffectiveNumThreads &&
"Number of threads cannot be increased!");
} else {
assert((KernelEnv.isGenericMode() || KernelEnv.isGenericSPMDMode()) &&
"Unexpected execution mode!");
// If we reach this point, then we have a non-combined construct, i.e.
// `teams distribute` with a nested `parallel for` and each block is
// assigned one iteration of the `distribute` loop. E.g.:
//
// #pragma omp target teams distribute
// for(...loop_tripcount...) {
// #pragma omp parallel for
// for(...) {}
// }
//
// Threads within a block will execute the iterations of the `parallel`
// loop.
TripCountNumBlocks = LoopTripCount;
}
}
uint32_t PreferredNumBlocks = TripCountNumBlocks;
// If the loops are long running we rather reuse blocks than spawn too many.
if (GenericDevice.getReuseBlocksForHighTripCount())
PreferredNumBlocks = std::min(TripCountNumBlocks, DefaultNumBlocks);
return std::min(PreferredNumBlocks,
GenericDevice.getBlockLimit(EffectiveNumThreads));
}
/// Build the base KernelLaunchArgsTy for a launch from the public
/// KernelArgsTy and the kernel's cached launch-geometry properties.
static KernelLaunchArgsTy buildLaunchArgs(const KernelArgsTy &KernelArgs,
KernelReplayOutcomeTy *ReplayOutcome,
const KernelLaunchInfoTy &KernelEnv) {
KernelLaunchArgsTy LaunchArgs;
LaunchArgs.OmpABIVersion = KernelArgs.Version;
LaunchArgs.ReplayOutcome = ReplayOutcome;
LaunchArgs.ArgSizes = KernelArgs.ArgSizes;
LaunchArgs.Tripcount = KernelArgs.Tripcount;
llvm::copy(KernelArgs.UserNumBlocks, LaunchArgs.UserNumBlocks);
// Save the requested value before computing the effective number of blocks so
// it can be used by record-replay mechanisms.
LaunchArgs.KernelLaunchInfo.RequestedNumBlocks = KernelArgs.UserNumBlocks[0];
LaunchArgs.KernelLaunchInfo.MaxNumThreads = KernelEnv.MaxNumThreads;
llvm::copy(KernelArgs.UserThreadLimit, LaunchArgs.UserThreadLimit);
LaunchArgs.Flags.Cooperative = KernelArgs.Flags.Cooperative;
return LaunchArgs;
}
/// Assert the launch geometry invariants expected by the plugin layer.
static void checkLaunchInvariants(const KernelLaunchArgsTy &LaunchArgs,
const KernelArgsTy &KernelArgs,
const KernelLaunchInfoTy &KernelLaunchInfo) {
// Multidimensional is only supported with bare mode for now.
assert(KernelLaunchInfo.isBareMode() ||
LaunchArgs.UserThreadLimit[1] == 1 &&
LaunchArgs.UserThreadLimit[2] == 1 &&
LaunchArgs.UserNumBlocks[1] == 1 &&
LaunchArgs.UserNumBlocks[2] == 1 &&
"Non-bare mode should only use the first thread and block "
"dimensions");
assert(!KernelArgs.Flags.StrictBlocks ||
LaunchArgs.UserNumBlocks[0] > 0 && LaunchArgs.UserNumBlocks[1] > 0 &&
LaunchArgs.UserNumBlocks[2] > 0 &&
"Strict requires number of blocks greater than zero");
assert(!KernelArgs.Flags.StrictThreads ||
LaunchArgs.UserThreadLimit[0] > 0 &&
LaunchArgs.UserThreadLimit[1] > 0 &&
LaunchArgs.UserThreadLimit[2] > 0 &&
"Strict requires number of threads greater than zero");
}
/// Calculate or adjust, in place, the effective number of threads and blocks
/// for the first dimension, unless the caller requested strict counts.
static void adjustEffectiveGeometry(GenericDeviceTy &GenericDevice,
KernelLaunchArgsTy &LaunchArgs,
const KernelArgsTy &KernelArgs,
const KernelLaunchInfoTy &KernelEnv) {
const bool StrictBlocks = KernelArgs.Flags.StrictBlocks;
const bool StrictThreads = KernelArgs.Flags.StrictThreads;
if (StrictThreads && StrictBlocks)
return;
assert(!KernelEnv.isBareMode() &&
"bare kernel launches must request strict thread/block counts");
// Record whether the user actually requested a thread limit (thread_limit
// clause) before possibly overwriting UserThreadLimit[0] below with the
// computed effective value.
const bool ThreadLimitFromUser = LaunchArgs.UserThreadLimit[0] > 0;
uint32_t EffectiveNumThreads = LaunchArgs.UserThreadLimit[0];
if (!StrictThreads)
EffectiveNumThreads =
getEffectiveNumThreads(GenericDevice, EffectiveNumThreads, KernelEnv);
if (!StrictBlocks)
LaunchArgs.UserNumBlocks[0] = getEffectiveNumBlocks(
GenericDevice, LaunchArgs.UserNumBlocks[0], LaunchArgs.Tripcount,
EffectiveNumThreads, StrictThreads, ThreadLimitFromUser, KernelEnv);
LaunchArgs.UserThreadLimit[0] = EffectiveNumThreads;
}
/// Flatten the kernel arguments into \p LaunchArgs.Args. Returns the address
/// of the element reserved for the kernel launch environment (dyn_ptr), or
/// null if this launch has no such slot.
static void **resolveArgsAndDynPtrSlot(KernelArgsTy &KernelArgs,
void **TgtVarsPtr, ptrdiff_t *TgtOffsets,
llvm::SmallVector<void *> &Args,
llvm::SmallVector<void *> &Ptrs,
llvm::SmallVector<int64_t> &ArgSizes,
KernelLaunchArgsTy &LaunchArgs) {
if (KernelArgs.Flags.IsCUDA) {
// Kernel languages (CUDA/HIP) pass an already-flattened argument-pointer
// array through KernelArgs.ArgPtrs instead of using the OpenMP
// base-pointer/offset argument scheme.
auto *LaunchParams =
reinterpret_cast<KernelLaunchParamsTy *>(KernelArgs.ArgPtrs);
LaunchArgs.NumArgs = LaunchParams->NumArgs;
LaunchArgs.Args = LaunchParams->Args;
return nullptr;
}
resolveKernelLaunchParams(TgtVarsPtr, TgtOffsets, KernelArgs.NumArgs, Args,
Ptrs, LaunchArgs);
if (KernelArgs.NumArgs == 0 ||
KernelArgs.Version < OMP_KERNEL_ARG_MIN_VERSION_WITH_DYN_PTR)
return nullptr;
// The dyn_ptr slot is reserved by the host (version >= 4) or by
// upgradeKernelArgs (version 3) as the last element of the argument array.
// Version 3 device kernels expect it first instead, so rotate it to the
// front to match that ABI.
if (KernelArgs.Version != OMP_KERNEL_ARG_MIN_VERSION_WITH_DYN_PTR)
return &Args[KernelArgs.NumArgs - 1];
std::rotate(Args.begin(), Args.end() - 1, Args.end());
// Keep ArgSizes in sync with the rotated Args, if present.
if (LaunchArgs.ArgSizes) {
ArgSizes.assign(LaunchArgs.ArgSizes,
LaunchArgs.ArgSizes + KernelArgs.NumArgs);
std::rotate(ArgSizes.begin(), ArgSizes.end() - 1, ArgSizes.end());
LaunchArgs.ArgSizes = ArgSizes.data();
}
return &Args[0];
}
/// Compute the dynamic block-memory configuration for this launch, filling in
/// \p LaunchArgs.DynCGroupMem with the native size to request, and, if this
/// launch has a reserved dyn_ptr slot (\p DynPtrSlot), the device-side kernel
/// launch environment.
static llvm::Error
prepareDynamicLaunchState(GenericDeviceTy &GenericDevice,
const KernelLaunchInfoTy &KernelEnv,
KernelLaunchArgsTy &LaunchArgs, uint32_t DynCGroupMem,
DynCGroupMemFallbackType DynCGroupMemFallback,
void **DynPtrSlot, AsyncInfoTy &AsyncInfo) {
uint32_t NumBlocksTotal = LaunchArgs.UserNumBlocks[0] *
LaunchArgs.UserNumBlocks[1] *
LaunchArgs.UserNumBlocks[2];
auto DynBlockMemConfOrErr =
prepareBlockMemory(GenericDevice, KernelEnv, DynCGroupMem,
DynCGroupMemFallback, NumBlocksTotal);
if (!DynBlockMemConfOrErr)
return DynBlockMemConfOrErr.takeError();
DynBlockMemConfTy &DynBlockMemConf = *DynBlockMemConfOrErr;
LaunchArgs.DynCGroupMem = DynBlockMemConf.NativeSize;
if (DynBlockMemConf.FallbackPtr)
freeAfterSynchronization(GenericDevice, AsyncInfo,
DynBlockMemConf.FallbackPtr, TARGET_ALLOC_DEVICE);
auto KernelLaunchEnvOrErr = getKernelLaunchEnvironment(
GenericDevice, LaunchArgs, KernelEnv, DynBlockMemConf, DynCGroupMem,
DynPtrSlot, AsyncInfo, LaunchArgs.UserNumBlocks[0]);
if (!KernelLaunchEnvOrErr)
return KernelLaunchEnvOrErr.takeError();
// Fill in the kernel launch environment (dyn_ptr) if this launch has a
// reserved slot for it. When replaying, getKernelLaunchEnvironment()
// returns null so the recorded value already in the slot is preserved.
if (DynPtrSlot && *KernelLaunchEnvOrErr)
*DynPtrSlot = *KernelLaunchEnvOrErr;
return llvm::Error::success();
}
// Run region on device
int32_t DeviceTy::launchKernel(void *TgtEntryPtr, void **TgtVarsPtr,
ptrdiff_t *TgtOffsets, KernelArgsTy &KernelArgs,
KernelReplayOutcomeTy *ReplayOutcome,
AsyncInfoTy &AsyncInfo) {
llvm::SmallVector<void *> Args, Ptrs;
llvm::SmallVector<int64_t> ArgSizes;
GenericDeviceTy &GenericDevice = RTL->getDevice(RTLDeviceID);
KernelLaunchInfoTy KernelLaunchInfo = getKernelLaunchInfo(TgtEntryPtr);
KernelLaunchArgsTy LaunchArgs =
buildLaunchArgs(KernelArgs, ReplayOutcome, KernelLaunchInfo);
checkLaunchInvariants(LaunchArgs, KernelArgs, KernelLaunchInfo);
adjustEffectiveGeometry(GenericDevice, LaunchArgs, KernelArgs,
KernelLaunchInfo);
void **DynPtrSlot = resolveArgsAndDynPtrSlot(
KernelArgs, TgtVarsPtr, TgtOffsets, Args, Ptrs, ArgSizes, LaunchArgs);
auto DynCGroupMemFallback = static_cast<DynCGroupMemFallbackType>(
KernelArgs.Flags.DynCGroupMemFallback);
if (auto Err = prepareDynamicLaunchState(
GenericDevice, KernelLaunchInfo, LaunchArgs, KernelArgs.DynCGroupMem,
DynCGroupMemFallback, DynPtrSlot, AsyncInfo)) {
REPORT() << "Failure to prepare launch state for kernel " << TgtEntryPtr
<< ": " << toString(std::move(Err));
return OFFLOAD_FAIL;
}
auto *Kernel = reinterpret_cast<GenericKernelTy *>(TgtEntryPtr);
INFO(OMP_INFOTYPE_PLUGIN_KERNEL, GenericDevice.getDeviceId(),
"Launching kernel %s with [%u,%u,%u] blocks and [%u,%u,%u] threads in "
"%s mode\n",
Kernel->getName(), LaunchArgs.UserNumBlocks[0],
LaunchArgs.UserNumBlocks[1], LaunchArgs.UserNumBlocks[2],
LaunchArgs.UserThreadLimit[0], LaunchArgs.UserThreadLimit[1],
LaunchArgs.UserThreadLimit[2], KernelLaunchInfo.getExecutionModeName());
return RTL->launch_kernel(RTLDeviceID, TgtEntryPtr, LaunchArgs, AsyncInfo);
}
// Run region on device
bool DeviceTy::printDeviceInfo() {
RTL->print_device_info(RTLDeviceID);
return true;
}
// Whether data can be copied to DstDevice directly
bool DeviceTy::isDataExchangable(const DeviceTy &DstDevice) {
if (RTL != DstDevice.RTL)
return false;
if (RTL->is_data_exchangable(RTLDeviceID, DstDevice.RTLDeviceID))
return true;
return false;
}
int32_t DeviceTy::synchronize(AsyncInfoTy &AsyncInfo) {
return RTL->synchronize(RTLDeviceID, AsyncInfo);
}
int32_t DeviceTy::queryAsync(AsyncInfoTy &AsyncInfo) {
return RTL->query_async(RTLDeviceID, AsyncInfo);
}
int32_t DeviceTy::createEvent(void **Event) {
return RTL->create_event(RTLDeviceID, Event);
}
int32_t DeviceTy::recordEvent(void *Event, AsyncInfoTy &AsyncInfo) {
return RTL->record_event(RTLDeviceID, Event, AsyncInfo);
}
int32_t DeviceTy::waitEvent(void *Event, AsyncInfoTy &AsyncInfo) {
return RTL->wait_event(RTLDeviceID, Event, AsyncInfo);
}
int32_t DeviceTy::syncEvent(void *Event) {
return RTL->sync_event(RTLDeviceID, Event);
}
int32_t DeviceTy::destroyEvent(void *Event) {
return RTL->destroy_event(RTLDeviceID, Event);
}
void DeviceTy::dumpOffloadEntries() {
fprintf(stderr, "Device %i offload entries:\n", DeviceID);
for (auto &It : *DeviceOffloadEntries.getExclusiveAccessor()) {
const char *Kind = "kernel";
if (It.second.isLink())
Kind = "link";
else if (It.second.isGlobal())
Kind = "global var.";
fprintf(stderr, " %11s: %s\n", Kind, It.second.getNameAsCStr());
}
}
bool DeviceTy::useAutoZeroCopy() {
if (PM->getRequirements() & OMP_REQ_UNIFIED_SHARED_MEMORY)
return false;
return RTL->use_auto_zero_copy(RTLDeviceID);
}
bool DeviceTy::isAccessiblePtr(const void *Ptr, size_t Size) {
return RTL->is_accessible_ptr(RTLDeviceID, Ptr, Size);
}