1616#include " ranges.h"
1717#include " region_map.h"
1818#include " types.h"
19+ #include < unordered_set>
1920
2021namespace celerity {
2122namespace detail {
@@ -62,7 +63,7 @@ namespace detail {
6263 * Essentially, this means that any requests made to the buffer_manager are assumed to be operations
6364 * that are currently allowed by the command graph.
6465 *
65- * FIXME: There are two important caveats that we need to deal with:
66+ * There are two important caveats that we need to deal with:
6667 *
6768 * - Reading from a buffer is no longer a const operation, as the buffer may need to be resized.
6869 * This means that two tasks that could be considered independent on a TDAG basis actually have an
@@ -75,6 +76,11 @@ namespace detail {
7576 * buffer first with "discard_write" and followed by a "read" should result in a combined "write" mode.
7677 * However the effect of the discard_write is recorded immediately, and the buffer_manager will thus
7778 * wrongly assume that no coherence update for the "read" is required.
79+ *
80+ * Currently, these issues are handled through the buffer locking mechanism.
81+ * See buffer_manager::try_lock, buffer_manager::unlock and buffer_manager::is_locked.
82+ *
83+ * FIXME: The current buffer locking mechanism limits task parallelism. Come up with a better solution.
7884 */
7985 class buffer_manager {
8086 public:
@@ -104,6 +110,8 @@ namespace detail {
104110 cl::sycl::id<Dims> offset;
105111 };
106112
113+ using buffer_lock_id = size_t ;
114+
107115 public:
108116 buffer_manager (device_queue& queue, buffer_lifecycle_callback lifecycle_cb);
109117
@@ -212,6 +220,8 @@ namespace detail {
212220 }
213221 }
214222
223+ audit_buffer_access (bid, new_buffer.is_allocated (), mode);
224+
215225 backing_buffer& target_buffer = new_buffer.is_allocated () ? new_buffer : old_buffer;
216226 const backing_buffer empty{};
217227 const backing_buffer& previous_buffer = new_buffer.is_allocated () ? old_buffer : empty;
@@ -242,6 +252,8 @@ namespace detail {
242252 }
243253 }
244254
255+ audit_buffer_access (bid, new_buffer.is_allocated (), mode);
256+
245257 backing_buffer& target_buffer = new_buffer.is_allocated () ? new_buffer : old_buffer;
246258 const backing_buffer empty{};
247259 const backing_buffer& previous_buffer = new_buffer.is_allocated () ? old_buffer : empty;
@@ -253,6 +265,32 @@ namespace detail {
253265 id_cast<Dims>(buffers[bid].host_buf .offset )};
254266 }
255267
268+ /* *
269+ * @brief Tries to lock the given list of @p buffers using the given lock @p id.
270+ *
271+ * If any of the buffers is currently locked, the locking attempt fails.
272+ *
273+ * Locking is currently an optional (opt-in) mechanism, i.e., buffers can also be
274+ * accessed without being locked. This is because locking is a bit of a band-aid fix
275+ * that doesn't properly cover all use-cases (for example, host-pointer initialized buffers).
276+ *
277+ * However, when accessing a locked buffer, the buffer_manager enforces additional
278+ * rules to ensure they are used in a safe manner for the duration of the lock:
279+ * - A locked buffer may only be resized at most once, and only for the first access.
280+ * - A locked buffer may not be accessed using consumer access modes, if it was previously
281+ * accessed using a pure producer mode.
282+ *
283+ * @returns Returns true if the list of buffers was successfully locked.
284+ */
285+ bool try_lock (buffer_lock_id, const std::unordered_set<buffer_id>& buffers);
286+
287+ /* *
288+ * Unlocks all buffers that were previously locked with a call to try_lock with the given @p id.
289+ */
290+ void unlock (buffer_lock_id id);
291+
292+ bool is_locked (buffer_id bid) const ;
293+
256294 private:
257295 struct backing_buffer {
258296 std::unique_ptr<buffer_storage> storage = nullptr ;
@@ -302,6 +340,15 @@ namespace detail {
302340 struct buffer_type_guard : buffer_type_guard_base {};
303341#endif
304342
343+ struct buffer_lock_info {
344+ bool is_locked = false ;
345+
346+ // For lack of a better name, this stores *an* access mode that has already been used during this lock.
347+ // While it initially stores whatever is first used to access the buffer, it will always be overwritten
348+ // by subsequent pure producer accesses, as those are the only ones we really care about.
349+ std::optional<cl::sycl::access::mode> earlier_access_mode = std::nullopt ;
350+ };
351+
305352 private:
306353 device_queue& queue;
307354 buffer_lifecycle_callback lifecycle_cb;
@@ -312,6 +359,9 @@ namespace detail {
312359 std::unordered_map<buffer_id, std::vector<transfer>> scheduled_transfers;
313360 std::unordered_map<buffer_id, region_map<data_location>> newest_data_location;
314361
362+ std::unordered_map<buffer_id, buffer_lock_info> buffer_lock_infos;
363+ std::unordered_map<buffer_lock_id, std::vector<buffer_id>> buffer_locks_by_id;
364+
315365#if !defined(NDEBUG)
316366 // Since we store buffers without type information (i.e., its data type and dimensionality),
317367 // it is the user's responsibility to only request access to a buffer using the correct type.
@@ -356,6 +406,15 @@ namespace detail {
356406 */
357407 void make_buffer_subrange_coherent (buffer_id bid, cl::sycl::access::mode mode, backing_buffer& target_buffer, const subrange<3 >& coherent_sr,
358408 const backing_buffer& previous_buffer = backing_buffer{});
409+
410+ /* *
411+ * Checks whether access to a currently locked buffer is safe.
412+ *
413+ * There's two distinct issues that can cause an access to be unsafe:
414+ * - If a buffer that has been accessed earlier needs to be resized (reallocated) now
415+ * - If a buffer was previously accessed using a discard_* mode and is now accessed using a consumer mode
416+ */
417+ void audit_buffer_access (buffer_id bid, bool requires_allocation, cl::sycl::access::mode mode);
359418 };
360419
361420} // namespace detail
0 commit comments