mempool.hh
Go to the documentation of this file.
1#pragma once
2#include <cstddef>
3#include <cstdint>
4#include <limits>
5#include <memory>
6#include <stdexcept>
9
10/// @file
11/// @brief BumpAllocator, the linear sub-allocator over a workspace span, and workspace_bytes().
12/// @see @ref design_workspace
13/// @ingroup api_workspace
14
15namespace batchlas {
16
17/// @brief Linear (bump) sub-allocator over a caller-supplied workspace buffer.
18///
19/// Each allocate() carves the next suitably aligned slice off the front of the buffer; nothing
20/// is ever freed individually. Every alignment is `max(16, MEM_BASE_ADDR_ALIGN / 8, alignof(T))`.
21///
22/// A pool made by measuring() is a *sizing* pool: it runs the same arithmetic over a fictitious
23/// region and reports through required_bytes() the buffer size the same call sequence needs.
24/// That is how every `*_buffer_size` query is computed (see workspace_bytes()).
25/// @warning Size a workspace with measuring() and required_bytes(), never by re-summing
26/// allocation_size() by hand: the capacity check uses the rounded size while the cursor
27/// advances by the raw size, so an exactly simulated total is too small.
28/// @ingroup api_workspace
29// evidence: docs/design/workspace.md#workspace-the-bumpallocator-alignment-trap
31 /// @brief A pool over `byte_size` bytes starting at `data`.
32 template <typename T>
33 BumpAllocator(T* data, size_t byte_size): data(data), byte_size(byte_size){}
34
35 /// @brief A pool over the bytes of `span`.
36 template <typename T>
37 BumpAllocator(Span<T> span): data(span.data()), byte_size(span.size()*sizeof(T)){}
38
39 /// @brief A sizing pool: an effectively unbounded, maximally aligned fictitious region.
40 ///
41 /// Give a real pool required_bytes() and the same call sequence succeeds, with at most one
42 /// alignment quantum to spare.
43 /// @warning The pointers it hands out are non-null and aligned (views can be built over them)
44 /// but unbacked: sizing code must never touch their contents, launch a kernel, or
45 /// test `data() != nullptr` to mean "a workspace was passed".
46 // Exact because every alignment is one uniform value (device_align >= 16 >= alignof(T)), so
47 // offsets from the base match a real pool's. evidence: docs/design/workspace.md#workspace-bumpallocator-sizing-mode
48 static BumpAllocator measuring() { return BumpAllocator(measure_tag{}); }
49
50 /// @brief True for a pool made by measuring().
51 inline bool is_measuring() const { return measuring_; }
52
53 /// @brief Bytes a real pool must be given for the call sequence so far to succeed.
54 ///
55 /// Rounded up to the coarsest alignment the sequence asked for.
56 /// @throws batchlas::api_misuse on a pool not made by measuring()
57 // Rounding is load-bearing: callers add a callee's size into their total and re-serve it
58 // with allocate<std::byte>(), which rounds again.
59 inline size_t required_bytes() const {
60 if (!measuring_) {
61 throw batchlas::api_misuse("BumpAllocator::required_bytes() on a real pool; use BumpAllocator::measuring().");
62 }
63 if (align_quantum_ == 0) return high_water_;
64 return (high_water_ + align_quantum_ - 1) & ~(align_quantum_ - 1);
65 }
66
67 /// @brief Alignment in bytes of an allocation of `T` on `device`.
68 template<typename T>
69 constexpr inline static auto alignment(const Device& device){
70 // 16-byte floor: vendors commonly require it and SYCL cannot query it.
71 auto device_align_bytes = std::max((size_t)16, (size_t)device.get_property(DeviceProperty::MEM_BASE_ADDR_ALIGN)/8);
72 return std::max(device_align_bytes, static_cast<std::uintptr_t>(alignof(T)));
73 }
74
75 /// @brief Bytes `size` elements of `T` occupy, rounded up to alignment<T>(); 0 for `size == 0`.
76 template<typename T>
77 constexpr inline static size_t allocation_size(const Device& device, size_t size){
78 if (size == 0) return 0; // Handle zero size allocation gracefully
79 std::uintptr_t total_size = size * sizeof(T);
80 return (total_size + alignment<T>(device) - 1) & ~(alignment<T>(device) - 1);
81 }
82
83 /// @brief allocation_size() for the Queue's device.
84 template<typename T>
85 constexpr inline static size_t allocation_size(Queue& ctx, size_t size) {return allocation_size<T>(ctx.device(), size);}
86
87 /// @brief Carve `size` elements of `T`, aligned to alignment<T>(), off the front of the pool.
88 /// @tparam T element type
89 /// @param device device whose alignment rule applies
90 /// @param size number of elements; 0 returns an empty Span and consumes nothing
91 /// @return the allocated elements (uninitialised; unbacked in a sizing pool)
92 /// @throws batchlas::workspace_error if the pool has fewer than allocation_size<T>() bytes left
93 template<typename T>
94 constexpr inline Span<T> allocate(const Device& device, size_t size){
95 if (size == 0) return {};
96 size_t alloc_size = allocation_size<T>(device,size);
97 if (alloc_size > byte_size){
98 throw batchlas::workspace_error("Attempted to allocate " + std::to_string(alloc_size) + " bytes from a BumpAllocator with only " + std::to_string(byte_size) + " bytes remaining.");
99 }
100
101 void* aligned = data;
102 size_t remaining = byte_size;
103 if (std::align(alignment<T>(device), size * sizeof(T), aligned, remaining) == nullptr) {
104 throw batchlas::workspace_error("Failed to align BumpAllocator storage for requested allocation.");
105 }
106
107 if (measuring_) {
108 // Record what a real pool must be GIVEN, not what it consumes: the check above uses
109 // the rounded size from the unaligned cursor, the advance below only the raw extent.
110 const auto base = static_cast<std::byte*>(measure_base_);
111 const size_t need_for_check = static_cast<size_t>(static_cast<std::byte*>(data) - base) + alloc_size;
112 const size_t need_for_data = static_cast<size_t>(static_cast<std::byte*>(aligned) - base) + size * sizeof(T);
113 high_water_ = std::max(high_water_, std::max(need_for_check, need_for_data));
114 align_quantum_ = std::max(align_quantum_, static_cast<size_t>(alignment<T>(device)));
115 }
116
117 auto* next = static_cast<std::byte*>(aligned) + size * sizeof(T);
118 T* ptr = static_cast<T*>(aligned);
119 byte_size -= static_cast<size_t>(next - static_cast<std::byte*>(data));
120 data = next;
121
122 return Span(ptr, size);
123 }
124
125 /// @brief allocate() with the Queue's device.
126 template<typename T>
127 constexpr inline Span<T> allocate(Queue& ctx, size_t size) {return allocate<T>(ctx.device(), size);}
128
129 /// @brief The still-unclaimed tail of the pool.
130 ///
131 /// Lets a callee sub-allocate without the caller knowing its size up front; pair with
132 /// consume() to hand back the bytes it actually took.
133 /// @throws batchlas::api_misuse on a sizing pool, whose extent is fictitious
134 inline Span<std::byte> remaining() const {
135 if (measuring_) {
136 // Callees sized against remaining().size() must be converted deliberately
137 // (see iluk / syevx_lobpcg), never implicitly.
138 throw batchlas::api_misuse("BumpAllocator::remaining() is not available in sizing mode.");
139 }
140 return Span<std::byte>(static_cast<std::byte*>(data), byte_size);
141 }
142
143 /// @brief Advance the pool by `bytes` (after a callee used remaining()).
144 /// @throws batchlas::workspace_error if fewer than `bytes` remain
145 inline void consume(size_t bytes) {
146 if (bytes > byte_size) {
147 throw batchlas::workspace_error("BumpAllocator::consume called with more bytes than remain.");
148 }
149 data = static_cast<std::byte*>(data) + bytes;
150 byte_size -= bytes;
151 }
152
153 private:
154
155 struct measure_tag {};
156
157 // 4 GiB-aligned (past any MEM_BASE_ADDR_ALIGN) and unmapped: a stray dereference faults.
158 static constexpr std::uintptr_t kMeasureBase = std::uintptr_t(1) << 32;
159
160 explicit BumpAllocator(measure_tag)
161 : data(reinterpret_cast<void*>(kMeasureBase)),
162 // No real request trips the check, and base + byte_size cannot wrap.
163 byte_size(std::numeric_limits<size_t>::max() / 4),
164 measuring_(true),
165 measure_base_(reinterpret_cast<void*>(kMeasureBase)) {}
166
167 void* data;
168 size_t byte_size;
169 bool measuring_ = false;
170 void* measure_base_ = nullptr;
171 size_t high_water_ = 0;
172 size_t align_quantum_ = 0;
173};
174
175/// @brief Bytes a workspace layout needs, obtained by replaying it against a sizing pool.
176///
177/// An algorithm describes its workspace once, in a `*_layout` function, and both its
178/// `*_buffer_size` entry point and its implementation go through it, so they cannot drift:
179/// @code
180/// size_t foo_buffer_size(Queue& ctx, ...) {
181/// return workspace_bytes([&](BumpAllocator& p) { return foo_layout<B, T>(ctx, p, ...); });
182/// }
183/// @endcode
184/// @param layout callable taking a `BumpAllocator&`; its result is discarded
185/// @return BumpAllocator::required_bytes() of the sizing pool after `layout` ran
186/// @pre `layout` is pure with respect to the workspace: it may read the caller's views and build
187/// views over what it allocates, but never reads or writes workspace memory, never launches
188/// a kernel, and asks nested size queries about the caller's views only.
189/// @see @ref design_workspace
190/// @ingroup api_workspace
191template <typename Fn>
192inline size_t workspace_bytes(Fn&& layout) {
193 auto sizer = BumpAllocator::measuring();
194 (void)layout(sizer);
195 return sizer.required_bytes();
196}
197
198} // namespace batchlas
199
200// Transitional global-scope shim; define BATCHLAS_NO_GLOBAL_NAMES to switch it off.
201// workspace_bytes MUST be shimmed: its only argument is a lambda, so ADL cannot find it.
202#ifndef BATCHLAS_NO_GLOBAL_NAMES
205#endif
A well-formed call arrived in a state, order or thread where it is not valid.
Definition error.hh:200
The scratch the operation was handed is too small, or the allocator ran out of it.
Definition error.hh:153
@ MEM_BASE_ADDR_ALIGN
Base address alignment (bits).
size_t workspace_bytes(Fn &&layout)
Bytes a workspace layout needs, obtained by replaying it against a sizing pool.
Definition mempool.hh:192
Span(T *, typename Span< T >::size_t) -> Span< T >
Linear (bump) sub-allocator over a caller-supplied workspace buffer.
Definition mempool.hh:30
size_t required_bytes() const
Bytes a real pool must be given for the call sequence so far to succeed.
Definition mempool.hh:59
static constexpr size_t allocation_size(Queue &ctx, size_t size)
allocation_size() for the Queue's device.
Definition mempool.hh:85
constexpr Span< T > allocate(Queue &ctx, size_t size)
allocate() with the Queue's device.
Definition mempool.hh:127
bool is_measuring() const
True for a pool made by measuring().
Definition mempool.hh:51
BumpAllocator(T *data, size_t byte_size)
A pool over byte_size bytes starting at data.
Definition mempool.hh:33
static constexpr size_t allocation_size(const Device &device, size_t size)
Bytes size elements of T occupy, rounded up to alignment<T>(); 0 for size == 0.
Definition mempool.hh:77
BumpAllocator(Span< T > span)
A pool over the bytes of span.
Definition mempool.hh:37
Span< std::byte > remaining() const
The still-unclaimed tail of the pool.
Definition mempool.hh:134
void consume(size_t bytes)
Advance the pool by bytes (after a callee used remaining()).
Definition mempool.hh:145
static constexpr auto alignment(const Device &device)
Alignment in bytes of an allocation of T on device.
Definition mempool.hh:69
constexpr Span< T > allocate(const Device &device, size_t size)
Carve size elements of T, aligned to alignment<T>(), off the front of the pool.
Definition mempool.hh:94
static BumpAllocator measuring()
A sizing pool: an effectively unbounded, maximally aligned fictitious region.
Definition mempool.hh:48
A handle to one SYCL device: its type and its index among devices of that type.
Where BatchLAS work runs: a device, a backend, an ordered SYCL queue and a workspace arena.
Device device() const
The device this Queue runs on.
Non-owning view of size() contiguous elements of type T.
Definition sycl-span.hh:39
constexpr Span()
An empty span (null data, size 0).
Definition sycl-span.hh:44
batchlas::Device, batchlas::Event and batchlas::Queue: where work runs and how it is ordered.
batchlas::Span, the non-owning pointer + length every entry point takes for 1-D data.