This patch is the first step towards introducing alignment support in memory allocations using liboffload, in order to enable SYCL implementation of aligned allocations. At the level of device allocators, it does not modify the Level Zero code except for forwarding the alignment parameter, since Level Zero already allows for specifying the alignment in its device allocator implementation. For AMD and CUDA, it checks whether the alignment passed by the caller is supported by the given backend; the reasoning behind this verification is described in the following paragraphs. At the API level, it adds a new function olMemAllocAligned, which is expected to work similarly to olMemAlloc, with the difference that the buffers returned by olMemAllocAligned should be aligned to the alignment passed by the user. At the level of the plugin interface internal abstractions, it adds a new argument Alignment to existing functions and delegates memory allocation between olMemAllocAligned and olMemAlloc implementations by using a common helper function. The goal of the anticipated series of patches is to implement handling of the alignment in the memory manager at the plugin interface level. At the first stage, presented in this patch, the information about the passed alignment is used mainly for checking whether the buffer returned by the device allocators meets the requirements. In the case of the requested memory size exceeding the thresholds of allocations handled by the memory manager, the request is forwarded directly to the device. Otherwise, the memory manager is responsible for allocating memory in full pages and pooling it according to the requested chunks. In the first scenario, the requested size, which is greater than the aforementioned threshold, is usually a multiple of the page size. Therefore, any alignment smaller than the page size would be correct. Neither CUDA nor HSA provides users with the ability to specify the alignment of the allocated memory. Their APIs include the alignment as one of the possible arguments only in functions that reserve virtual address space. CUDA enables users only to check the allocation granularity, which is usually synonymous with the page size. In the case of HSA, if the memory is allocated using a pool, the user is able to check not only the granularity, but also the alignment of the buffers allocated using the given pool. However, these values - granularity and alignment - are defined only if HSA_AMD_MEMORY_POOL_INFO_RUNTIME_ALLOC_ALLOWED is set to true, which should always be set since the current implementation of the memory pool for the AMD plugin in liboffload uses only hsa_amd_memory_pool_ts for allocations. Only Level Zero accepts the alignment parameter during the memory allocation process, but manages returned pointers internally. Since pooling is also implemented in the memory manager at the plugin level, such a design in the Level Zero device allocator duplicates pooling in the whole application. In the final version, the memory manager at the plugin interface level will handle memory pooling and the buffer alignment from different devices instead of delegating memory management to device allocator implementations, as this would simplify the Level Zero plugin design and provide AMD and CUDA plugins with support for the alignment in memory allocations, which is not natively included in their APIs.
381 lines
12 KiB
C++
381 lines
12 KiB
C++
//===----------- MemoryManager.h - Target independent memory manager ------===//
|
|
//
|
|
// 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// Target independent memory manager.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#ifndef LLVM_OPENMP_LIBOMPTARGET_PLUGINS_COMMON_MEMORYMANAGER_H
|
|
#define LLVM_OPENMP_LIBOMPTARGET_PLUGINS_COMMON_MEMORYMANAGER_H
|
|
|
|
#include <cassert>
|
|
#include <functional>
|
|
#include <list>
|
|
#include <mutex>
|
|
#include <set>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include "Shared/Debug.h"
|
|
#include "Shared/Utils.h"
|
|
#include "omptarget.h"
|
|
|
|
#include "llvm/Support/Error.h"
|
|
|
|
using namespace llvm::offload::debug;
|
|
|
|
namespace llvm {
|
|
|
|
/// Base class of per-device allocator.
|
|
class DeviceAllocatorTy {
|
|
public:
|
|
virtual ~DeviceAllocatorTy() = default;
|
|
|
|
/// Allocate a memory of size \p Size . \p HstPtr is used to assist the
|
|
/// allocation.
|
|
virtual Expected<void *> allocate(size_t Size, void *HstPtr,
|
|
TargetAllocTy Kind = TARGET_ALLOC_DEFAULT,
|
|
size_t Alignment = 0) = 0;
|
|
|
|
/// Delete the pointer \p TgtPtr on the device
|
|
virtual Error free(void *TgtPtr,
|
|
TargetAllocTy Kind = TARGET_ALLOC_DEFAULT) = 0;
|
|
};
|
|
|
|
/// Class of memory manager. The memory manager is per-device by using
|
|
/// per-device allocator. Therefore, each plugin using memory manager should
|
|
/// have an allocator for each device.
|
|
class MemoryManagerTy {
|
|
static constexpr const size_t BucketSize[] = {
|
|
0, 1U << 2, 1U << 3, 1U << 4, 1U << 5, 1U << 6, 1U << 7,
|
|
1U << 8, 1U << 9, 1U << 10, 1U << 11, 1U << 12, 1U << 13};
|
|
|
|
static constexpr const int NumBuckets =
|
|
sizeof(BucketSize) / sizeof(BucketSize[0]);
|
|
|
|
/// Find the previous number that is power of 2 given a number that is not
|
|
/// power of 2.
|
|
static size_t floorToPowerOfTwo(size_t Num) {
|
|
Num |= Num >> 1;
|
|
Num |= Num >> 2;
|
|
Num |= Num >> 4;
|
|
Num |= Num >> 8;
|
|
Num |= Num >> 16;
|
|
#if INTPTR_MAX == INT64_MAX
|
|
Num |= Num >> 32;
|
|
#elif INTPTR_MAX == INT32_MAX
|
|
// Do nothing with 32-bit
|
|
#else
|
|
#error Unsupported architecture
|
|
#endif
|
|
Num += 1;
|
|
return Num >> 1;
|
|
}
|
|
|
|
/// Find a suitable bucket
|
|
static int findBucket(size_t Size) {
|
|
const size_t F = floorToPowerOfTwo(Size);
|
|
|
|
ODBG(OLDT_Alloc) << "findBucket: Size " << Size << " is floored to " << F
|
|
<< ".";
|
|
|
|
int L = 0, H = NumBuckets - 1;
|
|
while (H - L > 1) {
|
|
int M = (L + H) >> 1;
|
|
if (BucketSize[M] == F)
|
|
return M;
|
|
if (BucketSize[M] > F)
|
|
H = M - 1;
|
|
else
|
|
L = M;
|
|
}
|
|
|
|
assert(L >= 0 && L < NumBuckets && "L is out of range");
|
|
|
|
ODBG(OLDT_Alloc) << "findBucket: Size " << Size << " goes to bucket " << L;
|
|
|
|
return L;
|
|
}
|
|
|
|
/// A structure stores the meta data of a target pointer
|
|
struct NodeTy {
|
|
/// Memory size
|
|
const size_t Size;
|
|
/// Target pointer
|
|
void *Ptr;
|
|
|
|
/// Constructor
|
|
NodeTy(size_t Size, void *Ptr) : Size(Size), Ptr(Ptr) {}
|
|
};
|
|
|
|
/// To make \p NodePtrTy ordered when they're put into \p std::multiset.
|
|
struct NodeCmpTy {
|
|
bool operator()(const NodeTy &LHS, const NodeTy &RHS) const {
|
|
return LHS.Size < RHS.Size;
|
|
}
|
|
};
|
|
|
|
/// A \p FreeList is a set of Nodes. We're using \p std::multiset here to make
|
|
/// the look up procedure more efficient.
|
|
using FreeListTy = std::multiset<std::reference_wrapper<NodeTy>, NodeCmpTy>;
|
|
|
|
/// A list of \p FreeListTy entries, each of which is a \p std::multiset of
|
|
/// Nodes whose size is less or equal to a specific bucket size.
|
|
std::vector<FreeListTy> FreeLists;
|
|
/// A list of mutex for each \p FreeListTy entry
|
|
std::vector<std::mutex> FreeListLocks;
|
|
/// A table to map from a target pointer to its node
|
|
std::unordered_map<void *, NodeTy> PtrToNodeTable;
|
|
/// The mutex for the table \p PtrToNodeTable
|
|
std::mutex MapTableLock;
|
|
|
|
/// The reference to a device allocator
|
|
DeviceAllocatorTy &DeviceAllocator;
|
|
|
|
/// The threshold to manage memory using memory manager. If the request size
|
|
/// is larger than \p SizeThreshold, the allocation will not be managed by the
|
|
/// memory manager.
|
|
size_t SizeThreshold = 1U << 13;
|
|
|
|
/// Request memory from target device
|
|
Expected<void *> allocateOnDevice(size_t Size, void *HstPtr,
|
|
size_t Alignment) const {
|
|
return DeviceAllocator.allocate(Size, HstPtr, TARGET_ALLOC_DEVICE,
|
|
Alignment);
|
|
}
|
|
|
|
/// Deallocate data on device
|
|
Error deleteOnDevice(void *Ptr) const { return DeviceAllocator.free(Ptr); }
|
|
|
|
/// This function is called when it tries to allocate memory on device but the
|
|
/// device returns out of memory. It will first free all memory in the
|
|
/// FreeList and try to allocate again.
|
|
Expected<void *> freeAndAllocate(size_t Size, void *HstPtr,
|
|
size_t Alignment) {
|
|
std::vector<void *> RemoveList;
|
|
|
|
// Deallocate all memory in FreeList
|
|
for (int I = 0; I < NumBuckets; ++I) {
|
|
FreeListTy &List = FreeLists[I];
|
|
std::lock_guard<std::mutex> Lock(FreeListLocks[I]);
|
|
if (List.empty())
|
|
continue;
|
|
for (const NodeTy &N : List) {
|
|
if (auto Err = deleteOnDevice(N.Ptr))
|
|
return Err;
|
|
RemoveList.push_back(N.Ptr);
|
|
}
|
|
FreeLists[I].clear();
|
|
}
|
|
|
|
// Remove all nodes in the map table which have been released
|
|
if (!RemoveList.empty()) {
|
|
std::lock_guard<std::mutex> LG(MapTableLock);
|
|
for (void *P : RemoveList)
|
|
PtrToNodeTable.erase(P);
|
|
}
|
|
|
|
// Try allocate memory again
|
|
return allocateOnDevice(Size, HstPtr, Alignment);
|
|
}
|
|
|
|
/// The goal is to allocate memory on the device. It first tries to
|
|
/// allocate directly on the device. If a \p nullptr is returned, it might
|
|
/// be because the device is OOM. In that case, it will free all unused
|
|
/// memory and then try again.
|
|
Expected<void *> allocateOrFreeAndAllocateOnDevice(size_t Size, void *HstPtr,
|
|
size_t Alignment) {
|
|
auto TgtPtrOrErr = allocateOnDevice(Size, HstPtr, Alignment);
|
|
if (!TgtPtrOrErr)
|
|
return TgtPtrOrErr.takeError();
|
|
|
|
void *TgtPtr = *TgtPtrOrErr;
|
|
// We cannot get memory from the device. It might be due to OOM. Let's
|
|
// free all memory in FreeLists and try again.
|
|
if (TgtPtr == nullptr) {
|
|
ODBG(OLDT_Alloc) << "Failed to get memory on device. Free all memory "
|
|
<< "in FreeLists and try again.";
|
|
TgtPtrOrErr = freeAndAllocate(Size, HstPtr, Alignment);
|
|
if (!TgtPtrOrErr)
|
|
return TgtPtrOrErr.takeError();
|
|
TgtPtr = *TgtPtrOrErr;
|
|
}
|
|
|
|
if (TgtPtr == nullptr)
|
|
ODBG(OLDT_Alloc) << "Still cannot get memory on device probably because "
|
|
<< "the device is OOM.";
|
|
|
|
return TgtPtr;
|
|
}
|
|
|
|
public:
|
|
/// Constructor. If \p Threshold is non-zero, then the default threshold will
|
|
/// be overwritten by \p Threshold.
|
|
MemoryManagerTy(DeviceAllocatorTy &DeviceAllocator, size_t Threshold = 0)
|
|
: FreeLists(NumBuckets), FreeListLocks(NumBuckets),
|
|
DeviceAllocator(DeviceAllocator) {
|
|
if (Threshold)
|
|
SizeThreshold = Threshold;
|
|
}
|
|
|
|
/// Destructor
|
|
~MemoryManagerTy() {
|
|
for (auto &PtrToNode : PtrToNodeTable) {
|
|
assert(PtrToNode.second.Ptr && "nullptr in map table");
|
|
if (auto Err = deleteOnDevice(PtrToNode.second.Ptr))
|
|
REPORT() << "Failure to delete memory: " << toString(std::move(Err));
|
|
}
|
|
}
|
|
|
|
/// Allocate memory of size \p Size from target device. \p HstPtr is used to
|
|
/// assist the allocation.
|
|
Expected<void *> allocate(size_t Size, void *HstPtr, size_t Alignment) {
|
|
// If the size is zero, we will not bother the target device. Just return
|
|
// nullptr directly.
|
|
if (Size == 0)
|
|
return nullptr;
|
|
|
|
ODBG(OLDT_Alloc) << "MemoryManagerTy::allocate: size " << Size
|
|
<< " with host pointer " << HstPtr << ".";
|
|
|
|
// If the size is greater than the threshold, allocate it directly from
|
|
// device.
|
|
if (Size > SizeThreshold) {
|
|
ODBG(OLDT_Alloc) << Size << " is greater than the threshold "
|
|
<< SizeThreshold << ". Allocate it directly from device";
|
|
auto TgtPtrOrErr =
|
|
allocateOrFreeAndAllocateOnDevice(Size, HstPtr, Alignment);
|
|
if (!TgtPtrOrErr)
|
|
return TgtPtrOrErr.takeError();
|
|
|
|
ODBG(OLDT_Alloc) << "Got target pointer " << *TgtPtrOrErr
|
|
<< ". Return directly.";
|
|
|
|
return *TgtPtrOrErr;
|
|
}
|
|
|
|
NodeTy *NodePtr = nullptr;
|
|
|
|
// Try to get a node from FreeList
|
|
{
|
|
const int B = findBucket(Size);
|
|
FreeListTy &List = FreeLists[B];
|
|
|
|
NodeTy TempNode(Size, nullptr);
|
|
std::lock_guard<std::mutex> LG(FreeListLocks[B]);
|
|
const auto Itr = List.find(TempNode);
|
|
|
|
if (Itr != List.end()) {
|
|
NodePtr = &Itr->get();
|
|
List.erase(Itr);
|
|
}
|
|
}
|
|
|
|
if (NodePtr != nullptr)
|
|
ODBG(OLDT_Alloc) << "Find one node " << NodePtr << " in the bucket.";
|
|
|
|
// We cannot find a valid node in FreeLists. Let's allocate on device and
|
|
// create a node for it.
|
|
if (NodePtr == nullptr) {
|
|
ODBG(OLDT_Alloc) << "Cannot find a node in the FreeLists. "
|
|
<< "Allocate on device.";
|
|
// Allocate one on device
|
|
auto TgtPtrOrErr =
|
|
allocateOrFreeAndAllocateOnDevice(Size, HstPtr, Alignment);
|
|
if (!TgtPtrOrErr)
|
|
return TgtPtrOrErr.takeError();
|
|
|
|
void *TgtPtr = *TgtPtrOrErr;
|
|
if (TgtPtr == nullptr)
|
|
return nullptr;
|
|
|
|
// Create a new node and add it into the map table
|
|
{
|
|
std::lock_guard<std::mutex> Guard(MapTableLock);
|
|
auto Itr = PtrToNodeTable.emplace(TgtPtr, NodeTy(Size, TgtPtr));
|
|
NodePtr = &Itr.first->second;
|
|
}
|
|
|
|
ODBG(OLDT_Alloc) << "Node address " << NodePtr << ", target pointer "
|
|
<< TgtPtr << ", size " << Size;
|
|
}
|
|
|
|
assert(NodePtr && "NodePtr should not be nullptr at this point");
|
|
|
|
return NodePtr->Ptr;
|
|
}
|
|
|
|
/// Deallocate memory pointed by \p TgtPtr
|
|
Error free(void *TgtPtr) {
|
|
ODBG(OLDT_Alloc) << "MemoryManagerTy::free: target memory " << TgtPtr
|
|
<< ".";
|
|
|
|
NodeTy *P = nullptr;
|
|
|
|
// Look it up into the table
|
|
{
|
|
std::lock_guard<std::mutex> G(MapTableLock);
|
|
auto Itr = PtrToNodeTable.find(TgtPtr);
|
|
|
|
// We don't remove the node from the map table because the map does not
|
|
// change.
|
|
if (Itr != PtrToNodeTable.end())
|
|
P = &Itr->second;
|
|
}
|
|
|
|
// The memory is not managed by the manager
|
|
if (P == nullptr) {
|
|
ODBG(OLDT_Alloc) << "Cannot find its node. Delete it on device directly.";
|
|
return deleteOnDevice(TgtPtr);
|
|
}
|
|
|
|
// Insert the node to the free list
|
|
const int B = findBucket(P->Size);
|
|
|
|
ODBG(OLDT_Alloc) << "Found its node " << P << ". Insert it to bucket " << B
|
|
<< ".";
|
|
|
|
{
|
|
std::lock_guard<std::mutex> G(FreeListLocks[B]);
|
|
FreeLists[B].insert(*P);
|
|
}
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
/// Get the size threshold from the environment variable
|
|
/// \p LIBOMPTARGET_MEMORY_MANAGER_THRESHOLD . Returns a <tt>
|
|
/// std::pair<size_t, bool> </tt> where the first element represents the
|
|
/// threshold and the second element represents whether user disables memory
|
|
/// manager explicitly by setting the var to 0. If user doesn't specify
|
|
/// anything, returns <0, true>.
|
|
static std::pair<size_t, bool> getSizeThresholdFromEnv() {
|
|
static UInt64Envar MemoryManagerThreshold(
|
|
"LIBOMPTARGET_MEMORY_MANAGER_THRESHOLD", 0);
|
|
|
|
size_t Threshold = MemoryManagerThreshold.get();
|
|
|
|
if (MemoryManagerThreshold.isPresent() && Threshold == 0) {
|
|
ODBG(OLDT_Alloc) << "Disabled memory manager as user set "
|
|
<< "LIBOMPTARGET_MEMORY_MANAGER_THRESHOLD=0.";
|
|
return std::make_pair(0, false);
|
|
}
|
|
|
|
return std::make_pair(Threshold, true);
|
|
}
|
|
};
|
|
|
|
// GCC still cannot handle the static data member like Clang so we still need
|
|
// this part.
|
|
constexpr const size_t MemoryManagerTy::BucketSize[];
|
|
constexpr const int MemoryManagerTy::NumBuckets;
|
|
|
|
} // namespace llvm
|
|
|
|
#endif // LLVM_OPENMP_LIBOMPTARGET_PLUGINS_COMMON_MEMORYMANAGER_H
|