CCCL (CUDA C++ Core Libraries) provides: - CUB: device/block/warp-level GPU primitives (reduce, scan, sort, topk) - Thrust: high-level parallel algorithms (transform_reduce, sort, scan) - libcudacxx: CUDA C++ standard library (atomics, barriers, memory) - cudax: experimental features (memory resources, allocators) - Tuning policies: per-SM hardware-specific algorithm parameters Competition optimization vectors mapped to CCCL: - Output TPS (83% weight): warp_reduce, block_reduce, device_topk - Input TPS (14% weight): device_scan, block_load, prefetch - Cache TPS (3% weight): prefix caching strategy patterns - Memory (0.9 util): pooled/cached/buddy allocators Source: https://github.com/NVIDIA/cccl (shallow clone, HEAD only) License: Apache-2.0
516 lines
17 KiB
C++
516 lines
17 KiB
C++
// SPDX-FileCopyrightText: Copyright (c) 2018, NVIDIA Corporation. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
/*! \file
|
|
* \brief A caching and pooling memory resource adaptor which uses separate upstream resources for memory allocation
|
|
* and bookkeeping.
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <thrust/detail/config.h>
|
|
|
|
#if defined(_CCCL_IMPLICIT_SYSTEM_HEADER_GCC)
|
|
# pragma GCC system_header
|
|
#elif defined(_CCCL_IMPLICIT_SYSTEM_HEADER_CLANG)
|
|
# pragma clang system_header
|
|
#elif defined(_CCCL_IMPLICIT_SYSTEM_HEADER_MSVC)
|
|
# pragma system_header
|
|
#endif // no system header
|
|
|
|
#include <thrust/detail/config.h>
|
|
|
|
#include <thrust/binary_search.h>
|
|
#include <thrust/detail/seq.h>
|
|
#include <thrust/find.h>
|
|
#include <thrust/host_vector.h>
|
|
#include <thrust/mr/allocator.h>
|
|
#include <thrust/mr/memory_resource.h>
|
|
#include <thrust/mr/pool_options.h>
|
|
|
|
#include <cuda/__cmath/ilog.h>
|
|
#include <cuda/__memory/is_valid_alignment.h>
|
|
#include <cuda/std/__algorithm/max.h>
|
|
#include <cuda/std/__algorithm/min.h>
|
|
#include <cuda/std/__host_stdlib/algorithm>
|
|
#include <cuda/std/cassert>
|
|
#include <cuda/std/cstdint>
|
|
|
|
THRUST_NAMESPACE_BEGIN
|
|
namespace mr
|
|
{
|
|
/** \addtogroup memory_resources Memory Resources
|
|
* \ingroup memory_management
|
|
* \{
|
|
*/
|
|
|
|
/*! A memory resource adaptor allowing for pooling and caching allocations from \p Upstream, using \p Bookkeeper for
|
|
* management of that cached and pooled memory, allowing to cache portions of memory inaccessible from the host.
|
|
*
|
|
* On a typical memory resource, calls to \p allocate and \p deallocate actually allocate and deallocate memory.
|
|
* Pooling memory resources only allocate and deallocate memory from an external resource (the upstream memory resource)
|
|
* when there's no suitable memory currently cached; otherwise, they use memory they have acquired beforehand, to make
|
|
* memory allocation faster and more efficient.
|
|
*
|
|
* The disjoint version of the pool resources uses a separate upstream memory resource, \p Bookkeeper, to allocate
|
|
* memory necessary to manage the cached memory. There may be many reasons to do that; the canonical one is that \p
|
|
* Upstream allocates memory that is inaccessible to the code of the pool resource, which means that it cannot embed the
|
|
* necessary information in memory obtained from \p Upstream; for instance, \p Upstream can be a CUDA non-managed memory
|
|
* resource, or a CUDA managed memory resource whose memory we would prefer to not migrate back and forth between
|
|
* host and device when executing bookkeeping code.
|
|
*
|
|
* This is not the only case where it makes sense to use a disjoint pool resource, though. In a multi-core environment
|
|
* it may be beneficial to avoid stealing cache lines from other cores by writing over bookkeeping information
|
|
* embedded in an allocated block of memory. In such a case, one can imagine wanting to use a disjoint pool where
|
|
* both the upstream and the bookkeeper are of the same type, to allocate memory consistently, but separately for
|
|
* those two purposes.
|
|
*
|
|
* \tparam Upstream the type of memory resources that will be used for allocating memory blocks to be handed off to the
|
|
* user \tparam Bookkeeper the type of memory resources that will be used for allocating bookkeeping memory
|
|
*/
|
|
template <typename Upstream, typename Bookkeeper>
|
|
class disjoint_unsynchronized_pool_resource final
|
|
: public memory_resource<typename Upstream::pointer>
|
|
, private validator2<Upstream, Bookkeeper>
|
|
{
|
|
public:
|
|
/*! Get the default options for a disjoint pool. These are meant to be a sensible set of values for many use cases,
|
|
* and as such, may be tuned in the future. This function is exposed so that creating a set of options that are
|
|
* just a slight departure from the defaults is easy.
|
|
*/
|
|
static pool_options get_default_options()
|
|
{
|
|
pool_options ret;
|
|
|
|
ret.min_blocks_per_chunk = 16;
|
|
ret.min_bytes_per_chunk = 1024;
|
|
ret.max_blocks_per_chunk = static_cast<std::size_t>(1) << 20;
|
|
ret.max_bytes_per_chunk = static_cast<std::size_t>(1) << 30;
|
|
|
|
ret.smallest_block_size = THRUST_MR_DEFAULT_ALIGNMENT;
|
|
ret.largest_block_size = static_cast<std::size_t>(1) << 20;
|
|
|
|
ret.alignment = THRUST_MR_DEFAULT_ALIGNMENT;
|
|
|
|
ret.cache_oversized = true;
|
|
|
|
ret.cached_size_cutoff_factor = 16;
|
|
ret.cached_alignment_cutoff_factor = 16;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*! Constructor.
|
|
*
|
|
* \param upstream the upstream memory resource for allocations
|
|
* \param bookkeeper the upstream memory resource for bookkeeping
|
|
* \param options pool options to use
|
|
*/
|
|
disjoint_unsynchronized_pool_resource(
|
|
Upstream* upstream, Bookkeeper* bookkeeper, pool_options options = get_default_options())
|
|
: m_upstream(upstream)
|
|
, m_bookkeeper(bookkeeper)
|
|
, m_options(options)
|
|
, m_smallest_block_log2(::cuda::ceil_ilog2(m_options.smallest_block_size))
|
|
, m_pools(m_bookkeeper)
|
|
, m_allocated(m_bookkeeper)
|
|
, m_cached_oversized(m_bookkeeper)
|
|
, m_oversized(m_bookkeeper)
|
|
{
|
|
assert(m_options.validate());
|
|
|
|
pointer_vector free(m_bookkeeper);
|
|
pool p(free);
|
|
m_pools.resize(::cuda::ceil_ilog2(m_options.largest_block_size) - m_smallest_block_log2 + 1, p);
|
|
}
|
|
|
|
// TODO: C++11: use delegating constructors
|
|
|
|
/*! Constructor. Upstream and bookkeeping resources are obtained by calling \p get_global_resource for their types.
|
|
*
|
|
* \param options pool options to use
|
|
*/
|
|
disjoint_unsynchronized_pool_resource(pool_options options = get_default_options())
|
|
: m_upstream(get_global_resource<Upstream>())
|
|
, m_bookkeeper(get_global_resource<Bookkeeper>())
|
|
, m_options(options)
|
|
, m_smallest_block_log2(::cuda::ceil_ilog2(m_options.smallest_block_size))
|
|
, m_pools(m_bookkeeper)
|
|
, m_allocated(m_bookkeeper)
|
|
, m_cached_oversized(m_bookkeeper)
|
|
, m_oversized(m_bookkeeper)
|
|
{
|
|
assert(m_options.validate());
|
|
|
|
pointer_vector free(m_bookkeeper);
|
|
pool p(free);
|
|
m_pools.resize(::cuda::ceil_ilog2(m_options.largest_block_size) - m_smallest_block_log2 + 1, p);
|
|
}
|
|
|
|
/*! Destructor. Releases all held memory to upstream.
|
|
*/
|
|
~disjoint_unsynchronized_pool_resource() override // NOLINT(bugprone-exception-escape)
|
|
{
|
|
release();
|
|
}
|
|
|
|
private:
|
|
using void_ptr = typename Upstream::pointer;
|
|
using char_ptr = typename ::cuda::std::pointer_traits<void_ptr>::template rebind<char>;
|
|
|
|
struct chunk_descriptor
|
|
{
|
|
std::size_t size;
|
|
void_ptr pointer;
|
|
std::size_t pool_idx;
|
|
};
|
|
|
|
using chunk_vector = thrust::host_vector<chunk_descriptor, allocator<chunk_descriptor, Bookkeeper>>;
|
|
|
|
struct oversized_block_descriptor
|
|
{
|
|
std::size_t size;
|
|
std::size_t alignment;
|
|
void_ptr pointer;
|
|
|
|
_CCCL_HOST_DEVICE bool operator==(const oversized_block_descriptor& other) const
|
|
{
|
|
return size == other.size && alignment == other.alignment && pointer == other.pointer;
|
|
}
|
|
|
|
_CCCL_HOST_DEVICE bool operator<(const oversized_block_descriptor& other) const
|
|
{
|
|
return size < other.size || (size == other.size && alignment < other.alignment);
|
|
}
|
|
};
|
|
|
|
struct equal_pointers
|
|
{
|
|
public:
|
|
_CCCL_HOST_DEVICE equal_pointers(void_ptr p)
|
|
: p(p)
|
|
{}
|
|
|
|
_CCCL_HOST_DEVICE bool operator()(const oversized_block_descriptor& desc) const
|
|
{
|
|
return desc.pointer == p;
|
|
}
|
|
|
|
private:
|
|
void_ptr p;
|
|
};
|
|
|
|
struct matching_alignment
|
|
{
|
|
public:
|
|
_CCCL_HOST_DEVICE matching_alignment(std::size_t requested)
|
|
: requested(requested)
|
|
{}
|
|
|
|
_CCCL_HOST_DEVICE bool operator()(const oversized_block_descriptor& desc) const
|
|
{
|
|
return desc.alignment >= requested;
|
|
}
|
|
|
|
private:
|
|
std::size_t requested;
|
|
};
|
|
|
|
using oversized_block_vector =
|
|
thrust::host_vector<oversized_block_descriptor, allocator<oversized_block_descriptor, Bookkeeper>>;
|
|
|
|
using pointer_vector = thrust::host_vector<void_ptr, allocator<void_ptr, Bookkeeper>>;
|
|
|
|
struct pool
|
|
{
|
|
_CCCL_HOST pool(pointer_vector free)
|
|
: free_blocks(::cuda::std::move(free))
|
|
{}
|
|
|
|
_CCCL_HOST pool(const pool& other)
|
|
: free_blocks(other.free_blocks)
|
|
, previous_allocated_count(other.previous_allocated_count)
|
|
{}
|
|
|
|
_CCCL_EXEC_CHECK_DISABLE
|
|
pool& operator=(const pool&) = default;
|
|
|
|
// If we = default this (even with _CCCL_HOST annotation), then nvcc will synthesize a
|
|
// different subobject destruction code for the pool vector below. I am not entirely sure
|
|
// why, but it chooses to instantiate it as host-device instead of host, then complains we
|
|
// cannot call host-device destructors from host-only
|
|
_CCCL_HOST ~pool() {} // NOLINT(modernize-use-equals-default)
|
|
|
|
pointer_vector free_blocks{};
|
|
std::size_t previous_allocated_count{};
|
|
};
|
|
|
|
using pool_vector = thrust::host_vector<pool, allocator<pool, Bookkeeper>>;
|
|
|
|
Upstream* m_upstream;
|
|
Bookkeeper* m_bookkeeper;
|
|
|
|
pool_options m_options;
|
|
std::size_t m_smallest_block_log2;
|
|
|
|
// buckets containing free lists for each pooled size
|
|
pool_vector m_pools;
|
|
// list of all allocations from upstream for the above
|
|
chunk_vector m_allocated;
|
|
// list of all cached oversized/overaligned blocks that have been returned to the pool to cache
|
|
oversized_block_vector m_cached_oversized;
|
|
// list of all oversized/overaligned allocations from upstream
|
|
oversized_block_vector m_oversized;
|
|
|
|
public:
|
|
/*! Releases all held memory to upstream.
|
|
*/
|
|
void release()
|
|
{
|
|
// reset the buckets
|
|
for (std::size_t i = 0; i < m_pools.size(); ++i)
|
|
{
|
|
m_pools[i].free_blocks.clear();
|
|
m_pools[i].previous_allocated_count = 0;
|
|
}
|
|
|
|
// deallocate memory allocated for the buckets
|
|
for (std::size_t i = 0; i < m_allocated.size(); ++i)
|
|
{
|
|
m_upstream->do_deallocate(m_allocated[i].pointer, m_allocated[i].size, m_options.alignment);
|
|
}
|
|
|
|
// deallocate cached oversized/overaligned memory
|
|
for (std::size_t i = 0; i < m_oversized.size(); ++i)
|
|
{
|
|
m_upstream->do_deallocate(m_oversized[i].pointer, m_oversized[i].size, m_oversized[i].alignment);
|
|
}
|
|
|
|
m_allocated.clear();
|
|
m_oversized.clear();
|
|
m_cached_oversized.clear();
|
|
}
|
|
|
|
void squeeze()
|
|
{
|
|
// Find all unused chunks and deallocate them
|
|
for (auto it = m_allocated.begin(); it != m_allocated.end();)
|
|
{
|
|
const auto pool_idx = (*it).pool_idx;
|
|
auto& pool = m_pools[pool_idx];
|
|
|
|
const std::size_t bytes_log2 = pool_idx + m_smallest_block_log2;
|
|
const std::size_t bucket_size = static_cast<std::size_t>(1) << bytes_log2;
|
|
const std::size_t n = (*it).size / bucket_size;
|
|
assert((*it).size % bucket_size == 0);
|
|
|
|
bool in_use = false;
|
|
for (std::size_t i = 0; i < n; ++i)
|
|
{
|
|
const auto ptr = static_cast<void_ptr>(static_cast<char_ptr>((*it).pointer) + i * bucket_size);
|
|
if (find(pool.free_blocks.begin(), pool.free_blocks.end(), ptr) == pool.free_blocks.end())
|
|
{
|
|
in_use = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!in_use)
|
|
{
|
|
// Remove all free blocks cut from this chunk:
|
|
for (std::size_t i = 0; i < n; ++i)
|
|
{
|
|
const auto ptr = static_cast<void_ptr>(static_cast<char_ptr>((*it).pointer) + i * bucket_size);
|
|
pool.free_blocks.erase(find(pool.free_blocks.begin(), pool.free_blocks.end(), ptr));
|
|
}
|
|
|
|
// Deallocate and remove this chunk from the list of allocated chunks
|
|
m_upstream->do_deallocate((*it).pointer, (*it).size, m_options.alignment);
|
|
it = m_allocated.erase(it);
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
|
|
// Remove all cached oversized allocations
|
|
for (auto it = m_cached_oversized.begin(); it != m_cached_oversized.end();)
|
|
{
|
|
m_upstream->do_deallocate((*it).pointer, (*it).size, (*it).alignment);
|
|
m_oversized.erase(find(m_oversized.begin(), m_oversized.end(), *it));
|
|
it = m_cached_oversized.erase(it);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] void_ptr do_allocate(std::size_t bytes, std::size_t alignment = THRUST_MR_DEFAULT_ALIGNMENT) override
|
|
{
|
|
try
|
|
{
|
|
return do_allocate_impl(bytes, alignment);
|
|
}
|
|
catch (std::bad_alloc&)
|
|
{
|
|
this->squeeze();
|
|
}
|
|
|
|
return do_allocate_impl(bytes, alignment);
|
|
}
|
|
|
|
[[nodiscard]] void_ptr do_allocate_impl(std::size_t bytes, std::size_t alignment)
|
|
{
|
|
bytes = (std::max) (bytes, m_options.smallest_block_size);
|
|
assert(::cuda::__is_valid_alignment(alignment));
|
|
|
|
// an oversized and/or overaligned allocation requested; needs to be allocated separately
|
|
if (bytes > m_options.largest_block_size || alignment > m_options.alignment)
|
|
{
|
|
oversized_block_descriptor oversized;
|
|
oversized.size = bytes;
|
|
oversized.alignment = alignment;
|
|
|
|
if (m_options.cache_oversized && !m_cached_oversized.empty())
|
|
{
|
|
typename oversized_block_vector::iterator it =
|
|
thrust::lower_bound(thrust::seq, m_cached_oversized.begin(), m_cached_oversized.end(), oversized);
|
|
|
|
// if the size is bigger than the requested size by a factor
|
|
// bigger than or equal to the specified cutoff for size,
|
|
// allocate a new block
|
|
if (it != m_cached_oversized.end())
|
|
{
|
|
std::size_t size_factor = (*it).size / bytes;
|
|
if (size_factor >= m_options.cached_size_cutoff_factor)
|
|
{
|
|
it = m_cached_oversized.end();
|
|
}
|
|
}
|
|
|
|
if (it != m_cached_oversized.end() && (*it).alignment < alignment)
|
|
{
|
|
it = find_if(it + 1, m_cached_oversized.end(), matching_alignment(alignment));
|
|
}
|
|
|
|
// if the alignment is bigger than the requested one by a factor
|
|
// bigger than or equal to the specified cutoff for alignment,
|
|
// allocate a new block
|
|
if (it != m_cached_oversized.end())
|
|
{
|
|
std::size_t alignment_factor = (*it).alignment / alignment;
|
|
if (alignment_factor >= m_options.cached_alignment_cutoff_factor)
|
|
{
|
|
it = m_cached_oversized.end();
|
|
}
|
|
}
|
|
|
|
if (it != m_cached_oversized.end())
|
|
{
|
|
oversized.pointer = (*it).pointer;
|
|
m_cached_oversized.erase(it);
|
|
return oversized.pointer;
|
|
}
|
|
}
|
|
|
|
// no fitting cached block found; allocate a new one that's just up to the specs
|
|
oversized.pointer = m_upstream->do_allocate(bytes, alignment);
|
|
m_oversized.push_back(oversized);
|
|
|
|
return oversized.pointer;
|
|
}
|
|
|
|
// the request is NOT for oversized and/or overaligned memory
|
|
// allocate a block from an appropriate bucket
|
|
std::size_t bytes_log2 = ::cuda::ceil_ilog2(bytes);
|
|
std::size_t pool_idx = bytes_log2 - m_smallest_block_log2;
|
|
pool& bucket = m_pools[pool_idx];
|
|
|
|
// if the free list of the bucket has no elements, allocate a new chunk
|
|
// and split it into blocks pushed to the free list
|
|
if (bucket.free_blocks.empty())
|
|
{
|
|
std::size_t bucket_size = static_cast<std::size_t>(1) << bytes_log2;
|
|
|
|
std::size_t n = bucket.previous_allocated_count;
|
|
if (n == 0)
|
|
{
|
|
n = (::cuda::std::max) (m_options.min_blocks_per_chunk, //
|
|
m_options.min_bytes_per_chunk >> bytes_log2);
|
|
}
|
|
else
|
|
{
|
|
n = (::cuda::std::min) ({n * 3 / 2, //
|
|
m_options.max_bytes_per_chunk >> bytes_log2,
|
|
m_options.max_blocks_per_chunk});
|
|
}
|
|
|
|
bytes = n << bytes_log2;
|
|
|
|
assert(n >= m_options.min_blocks_per_chunk);
|
|
assert(n <= m_options.max_blocks_per_chunk);
|
|
assert(bytes >= m_options.min_bytes_per_chunk);
|
|
assert(bytes <= m_options.max_bytes_per_chunk);
|
|
|
|
chunk_descriptor allocated;
|
|
allocated.size = bytes;
|
|
allocated.pointer = m_upstream->do_allocate(bytes, m_options.alignment);
|
|
allocated.pool_idx = pool_idx;
|
|
m_allocated.push_back(allocated);
|
|
bucket.previous_allocated_count = n;
|
|
|
|
for (std::size_t i = 0; i < n; ++i)
|
|
{
|
|
bucket.free_blocks.push_back(static_cast<void_ptr>(static_cast<char_ptr>(allocated.pointer) + i * bucket_size));
|
|
}
|
|
}
|
|
|
|
// allocate a block from the front of the bucket's free list
|
|
void_ptr ret = bucket.free_blocks.back();
|
|
bucket.free_blocks.pop_back();
|
|
return ret;
|
|
}
|
|
|
|
void do_deallocate(void_ptr p, std::size_t n, std::size_t alignment = THRUST_MR_DEFAULT_ALIGNMENT) override
|
|
{
|
|
n = (std::max) (n, m_options.smallest_block_size);
|
|
assert(::cuda::__is_valid_alignment(alignment));
|
|
|
|
// verify that the pointer is at least as aligned as claimed
|
|
assert(reinterpret_cast<::cuda::std::intmax_t>(::cuda::std::to_address(p)) % alignment == 0);
|
|
|
|
// the deallocated block is oversized and/or overaligned
|
|
if (n > m_options.largest_block_size || alignment > m_options.alignment)
|
|
{
|
|
typename oversized_block_vector::iterator it = find_if(m_oversized.begin(), m_oversized.end(), equal_pointers(p));
|
|
assert(it != m_oversized.end());
|
|
|
|
oversized_block_descriptor oversized = *it;
|
|
|
|
if (m_options.cache_oversized)
|
|
{
|
|
typename oversized_block_vector::iterator position =
|
|
lower_bound(m_cached_oversized.begin(), m_cached_oversized.end(), oversized);
|
|
m_cached_oversized.insert(position, oversized);
|
|
return;
|
|
}
|
|
|
|
m_oversized.erase(it);
|
|
|
|
m_upstream->do_deallocate(p, oversized.size, oversized.alignment);
|
|
|
|
return;
|
|
}
|
|
|
|
// push the block to the front of the appropriate bucket's free list
|
|
std::size_t n_log2 = ::cuda::ceil_ilog2(n);
|
|
std::size_t pool_idx = n_log2 - m_smallest_block_log2;
|
|
pool& bucket = m_pools[pool_idx];
|
|
|
|
bucket.free_blocks.push_back(p);
|
|
}
|
|
};
|
|
|
|
/*! \} // memory_resource
|
|
*/
|
|
} // namespace mr
|
|
THRUST_NAMESPACE_END
|