From f954727a5e0e56d1625f94b367ba3749bd53a0fe Mon Sep 17 00:00:00 2001 From: Brandon Echols Date: Fri, 21 Aug 2026 11:29:07 -0700 Subject: [PATCH 1/5] Adds cleanup functions for resources --- docs/sphinx/user_guide/feature/resource.rst | 19 ++++ include/camp/resource.hpp | 35 ++++++ include/camp/resource/cuda.hpp | 85 +++++++++++---- include/camp/resource/hip.hpp | 83 ++++++++++---- include/camp/resource/host.hpp | 3 + include/camp/resource/omp_target.hpp | 3 + include/camp/resource/sycl.hpp | 3 + test/resource.cpp | 115 ++++++++++++++++++++ 8 files changed, 306 insertions(+), 40 deletions(-) diff --git a/docs/sphinx/user_guide/feature/resource.rst b/docs/sphinx/user_guide/feature/resource.rst index 9d897723..f84cf070 100644 --- a/docs/sphinx/user_guide/feature/resource.rst +++ b/docs/sphinx/user_guide/feature/resource.rst @@ -183,3 +183,22 @@ a unique device stream or queue will get a hash. However, since all Host resourc one `stream of execution` on the Host), users should be cautious when using the Host resource as a key. For example, ``std::unordered_map map`` would only ever have one entry. +Cleaning Up Managed Streams +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Every concrete resource backend provides a ``cleanup()`` function, and +``camp::resources::cleanup()`` calls it for every enabled backend. The +cleanup functions are not thread-safe. Resources that do not currently +require explicit runtime destruction will have no-ops for their respective +cleanup functions. + +CUDA and HIP resources are non-owning views of streams. Their cleanup +functions release Camp-created streams. Streams supplied through +``CudaFromStream()`` or ``HipFromStream()`` remain owned by the caller and are +not destroyed. + +Cleanup invalidates every existing resource that refers to a Camp-managed +stream. Applications must finish using those resources and ensure that no +other thread is using them before cleanup. A later resource construction will +recreate the managed streams, but it does not make an older resource valid +again. diff --git a/include/camp/resource.hpp b/include/camp/resource.hpp index d6ddffc6..a9dd40d5 100644 --- a/include/camp/resource.hpp +++ b/include/camp/resource.hpp @@ -417,6 +417,41 @@ struct hash { #include "camp/resource/omp_target.hpp" #endif +namespace camp +{ +namespace resources +{ + inline namespace v1 + { + /** + * \brief Clean up resources managed by all enabled backends. + * + * Existing resources that refer to backend-managed streams are invalid + * after this call. Caller-owned resources are not affected. The caller + * must ensure no other thread is using backend resources while cleanup + * runs. + */ + inline void cleanup() + { + Host::cleanup(); +#if defined(CAMP_HAVE_CUDA) + Cuda::cleanup(); +#endif +#if defined(CAMP_HAVE_HIP) + Hip::cleanup(); +#endif +#if defined(CAMP_HAVE_SYCL) + Sycl::cleanup(); +#endif +#if defined(CAMP_HAVE_OMP_OFFLOAD) + Omp::cleanup(); +#endif + } + + } // namespace v1 +} // namespace resources +} // namespace camp + #include "camp/resource/resource_allocator.hpp" #endif /* __CAMP_RESOURCE_HPP */ diff --git a/include/camp/resource/cuda.hpp b/include/camp/resource/cuda.hpp index 9e7e20ce..aa4840ae 100644 --- a/include/camp/resource/cuda.hpp +++ b/include/camp/resource/cuda.hpp @@ -152,27 +152,38 @@ namespace resources class Cuda { + static constexpr int num_streams = 16; + + struct stream_state { + std::array streams{}; + cudaStream_t default_stream = nullptr; + int previous = num_streams - 1; + std::mutex mutex; + }; + + static stream_state& get_stream_state() + { + static stream_state state; + return state; + } + static cudaStream_t get_a_stream(int num) { - static constexpr int num_streams = 16; - static std::array s_streams = [] { - std::array streams; - for (auto& s : streams) { + auto& state = get_stream_state(); + std::lock_guard lock(state.mutex); + + for (auto& s : state.streams) { + if (s == nullptr) { CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, &s); } - return streams; - }(); - - static std::mutex s_mtx; - static int s_previous = num_streams - 1; + } if (num < 0) { - std::lock_guard lock(s_mtx); - s_previous = (s_previous + 1) % num_streams; - return s_streams[s_previous]; + state.previous = (state.previous + 1) % num_streams; + return state.streams[state.previous]; } - return s_streams[num % num_streams]; + return state.streams[num % num_streams]; } // Private from-stream constructor @@ -226,16 +237,50 @@ namespace resources static Cuda get_default() { - static Cuda c([] { - cudaStream_t s; #if CAMP_USE_PLATFORM_DEFAULT_STREAM - s = 0; + return Cuda(nullptr); #else - CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, &s); + auto& state = get_stream_state(); + std::lock_guard lock(state.mutex); + if (state.default_stream == nullptr) { + CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, + &state.default_stream); + } + return Cuda(state.default_stream); +#endif + } + + /** + * \brief Destroy all CUDA streams created and managed by CAMP. + * + * Existing resources that refer to CAMP-managed streams are invalid + * after this call. Streams passed to CudaFromStream and the CUDA + * platform default stream are not destroyed. This function may be + * called repeatedly, and later resource construction recreates the + * managed streams. + * + * The caller must ensure no other thread is using CUDA resources while + * cleanup runs. + */ + static void cleanup() + { + auto& state = get_stream_state(); + + for (auto& s : state.streams) { + if (s != nullptr) { + CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamDestroy, s); + s = nullptr; + } + } + state.previous = num_streams - 1; + +#if !CAMP_USE_PLATFORM_DEFAULT_STREAM + if (state.default_stream != nullptr) { + CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamDestroy, + state.default_stream); + state.default_stream = nullptr; + } #endif - return s; - }()); - return c; } CudaEvent get_event() diff --git a/include/camp/resource/hip.hpp b/include/camp/resource/hip.hpp index 5aa22261..55159b93 100644 --- a/include/camp/resource/hip.hpp +++ b/include/camp/resource/hip.hpp @@ -153,27 +153,38 @@ namespace resources class Hip { + static constexpr int num_streams = 16; + + struct stream_state { + std::array streams{}; + hipStream_t default_stream = nullptr; + int previous = num_streams - 1; + std::mutex mutex; + }; + + static stream_state& get_stream_state() + { + static stream_state state; + return state; + } + static hipStream_t get_a_stream(int num) { - static constexpr int num_streams = 16; - static std::array s_streams = [] { - std::array streams; - for (auto& s : streams) { + auto& state = get_stream_state(); + std::lock_guard lock(state.mutex); + + for (auto& s : state.streams) { + if (s == nullptr) { CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &s); } - return streams; - }(); - - static std::mutex s_mtx; - static int s_previous = num_streams - 1; + } if (num < 0) { - std::lock_guard lock(s_mtx); - s_previous = (s_previous + 1) % num_streams; - return s_streams[s_previous]; + state.previous = (state.previous + 1) % num_streams; + return state.streams[state.previous]; } - return s_streams[num % num_streams]; + return state.streams[num % num_streams]; } // Private from-stream constructor @@ -224,16 +235,48 @@ namespace resources static Hip get_default() { - static Hip h([] { - hipStream_t s; #if CAMP_USE_PLATFORM_DEFAULT_STREAM - s = 0; + return Hip(nullptr); #else - CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &s); + auto& state = get_stream_state(); + std::lock_guard lock(state.mutex); + if (state.default_stream == nullptr) { + CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &state.default_stream); + } + return Hip(state.default_stream); +#endif + } + + /** + * \brief Destroy all HIP streams created and managed by CAMP. + * + * Existing resources that refer to CAMP-managed streams are invalid + * after this call. Streams passed to HipFromStream and the HIP platform + * default stream are not destroyed. This function may be called + * repeatedly, and later resource construction recreates the managed + * streams. + * + * The caller must ensure no other thread is using HIP resources while + * cleanup runs. + */ + static void cleanup() + { + auto& state = get_stream_state(); + + for (auto& s : state.streams) { + if (s != nullptr) { + CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamDestroy, s); + s = nullptr; + } + } + state.previous = num_streams - 1; + +#if !CAMP_USE_PLATFORM_DEFAULT_STREAM + if (state.default_stream != nullptr) { + CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamDestroy, state.default_stream); + state.default_stream = nullptr; + } #endif - return s; - }()); - return h; } HipEvent get_event() diff --git a/include/camp/resource/host.hpp b/include/camp/resource/host.hpp index cfa1d10d..58c5d736 100644 --- a/include/camp/resource/host.hpp +++ b/include/camp/resource/host.hpp @@ -93,6 +93,9 @@ namespace resources return h; } + /// Clean up resources managed by the host backend. + static void cleanup() {} + HostEvent get_event() { return HostEvent(); } Event get_event_erased() { return Event{get_event()}; } diff --git a/include/camp/resource/omp_target.hpp b/include/camp/resource/omp_target.hpp index c5f8a199..d98a5595 100644 --- a/include/camp/resource/omp_target.hpp +++ b/include/camp/resource/omp_target.hpp @@ -181,6 +181,9 @@ namespace resources return o; } + /// Clean up resources managed by the OpenMP target backend. + static void cleanup() {} + OmpEvent get_event() { return OmpEvent(addr, dev); } Event get_event_erased() { return Event{get_event()}; } diff --git a/include/camp/resource/sycl.hpp b/include/camp/resource/sycl.hpp index 6f32a0fc..ae40592b 100644 --- a/include/camp/resource/sycl.hpp +++ b/include/camp/resource/sycl.hpp @@ -308,6 +308,9 @@ namespace resources // get default resource static Sycl get_default() { return Sycl(0, get_default_context()); } + /// Clean up resources managed by the SYCL backend. + static void cleanup() {} + // Methods Platform get_platform() const { return Platform::sycl; } diff --git a/test/resource.cpp b/test/resource.cpp index b4ab0904..a4f81da5 100644 --- a/test/resource.cpp +++ b/test/resource.cpp @@ -1877,3 +1877,118 @@ TEST(CampResourceSycl, Helpers) Sycl::set_thread_default_context(original_thread); } #endif + +TEST(CampResource, Cleanup) +{ + Host::cleanup(); +#ifdef CAMP_HAVE_CUDA + { + Cuda cuda_resource; + Cuda cuda_default = Cuda::get_default(); + cuda_resource.wait(); + cuda_default.wait(); + } +#endif +#ifdef CAMP_HAVE_HIP + { + Hip hip_resource; + Hip hip_default = Hip::get_default(); + hip_resource.wait(); + hip_default.wait(); + } +#endif +#ifdef CAMP_HAVE_SYCL + Sycl::cleanup(); +#endif +#ifdef CAMP_HAVE_OMP_OFFLOAD + Omp::cleanup(); +#endif + + camp::resources::cleanup(); + camp::resources::cleanup(); + +#ifdef CAMP_HAVE_CUDA + { + Cuda recreated_cuda_resource; + Cuda recreated_cuda_default = Cuda::get_default(); + recreated_cuda_resource.wait(); + recreated_cuda_default.wait(); + } + Cuda::cleanup(); +#endif +#ifdef CAMP_HAVE_HIP + { + Hip recreated_hip_resource; + Hip recreated_hip_default = Hip::get_default(); + recreated_hip_resource.wait(); + recreated_hip_default.wait(); + } + Hip::cleanup(); +#endif +} + +#ifdef CAMP_HAVE_CUDA +TEST(CampResourceCuda, CleanupPreservesCustomStream) +{ + int current_device = -1; + cudaStream_t stream; + CAMP_CUDA_API_INVOKE_AND_CHECK(cudaGetDevice, ¤t_device); + CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, &stream); + + Cuda custom = Cuda::CudaFromStream(stream, current_device); + { + Cuda managed; + Cuda managed_default = Cuda::get_default(); + managed.wait(); + managed_default.wait(); + } + + Cuda::cleanup(); + Cuda::cleanup(); + custom.wait(); + + { + Cuda recreated; + Cuda recreated_default = Cuda::get_default(); + recreated.wait(); + recreated_default.wait(); + } + + camp::resources::cleanup(); + custom.wait(); + CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamDestroy, stream); +} +#endif + +#ifdef CAMP_HAVE_HIP +TEST(CampResourceHip, CleanupPreservesCustomStream) +{ + int current_device = -1; + hipStream_t stream; + CAMP_HIP_API_INVOKE_AND_CHECK(hipGetDevice, ¤t_device); + CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &stream); + + Hip custom = Hip::HipFromStream(stream, current_device); + { + Hip managed; + Hip managed_default = Hip::get_default(); + managed.wait(); + managed_default.wait(); + } + + Hip::cleanup(); + Hip::cleanup(); + custom.wait(); + + { + Hip recreated; + Hip recreated_default = Hip::get_default(); + recreated.wait(); + recreated_default.wait(); + } + + camp::resources::cleanup(); + custom.wait(); + CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamDestroy, stream); +} +#endif From ac6630b7de3acddf12b910a4a13266a28031250e Mon Sep 17 00:00:00 2001 From: Brandon Echols Date: Mon, 24 Aug 2026 10:12:06 -0700 Subject: [PATCH 2/5] Updates docs --- docs/sphinx/user_guide/feature/resource.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/user_guide/feature/resource.rst b/docs/sphinx/user_guide/feature/resource.rst index f84cf070..fb88cf3f 100644 --- a/docs/sphinx/user_guide/feature/resource.rst +++ b/docs/sphinx/user_guide/feature/resource.rst @@ -187,10 +187,9 @@ Cleaning Up Managed Streams ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Every concrete resource backend provides a ``cleanup()`` function, and -``camp::resources::cleanup()`` calls it for every enabled backend. The -cleanup functions are not thread-safe. Resources that do not currently -require explicit runtime destruction will have no-ops for their respective -cleanup functions. +``camp::resources::cleanup()`` calls it for every enabled backend. +Resources that do not currently require explicit runtime destruction +will have no-ops for their respective cleanup functions. CUDA and HIP resources are non-owning views of streams. Their cleanup functions release Camp-created streams. Streams supplied through @@ -202,3 +201,6 @@ stream. Applications must finish using those resources and ensure that no other thread is using them before cleanup. A later resource construction will recreate the managed streams, but it does not make an older resource valid again. + +.. note:: + The ``cleanup()`` function is not thread-safe. From 91f66223f2471ce19e5b5bcbc63423da0d580897 Mon Sep 17 00:00:00 2001 From: Brandon Echols Date: Mon, 24 Aug 2026 10:34:18 -0700 Subject: [PATCH 3/5] Updates docs adjusting comments --- docs/sphinx/user_guide/feature/resource.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/sphinx/user_guide/feature/resource.rst b/docs/sphinx/user_guide/feature/resource.rst index fb88cf3f..deef0b8b 100644 --- a/docs/sphinx/user_guide/feature/resource.rst +++ b/docs/sphinx/user_guide/feature/resource.rst @@ -183,18 +183,15 @@ a unique device stream or queue will get a hash. However, since all Host resourc one `stream of execution` on the Host), users should be cautious when using the Host resource as a key. For example, ``std::unordered_map map`` would only ever have one entry. -Cleaning Up Managed Streams -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Cleaning Up Resources +^^^^^^^^^^^^^^^^^^^^^ -Every concrete resource backend provides a ``cleanup()`` function, and -``camp::resources::cleanup()`` calls it for every enabled backend. +The ``cleanup`` function will delete/destroy any Camp-managed global state +that is used by resources. For example, this includes Cuda/Hip streams managed +by Camp. Every concrete resource backend provides a ``cleanup()`` function, +and ``camp::resources::cleanup()`` calls ``cleanup()`` for every enabled backend. Resources that do not currently require explicit runtime destruction -will have no-ops for their respective cleanup functions. - -CUDA and HIP resources are non-owning views of streams. Their cleanup -functions release Camp-created streams. Streams supplied through -``CudaFromStream()`` or ``HipFromStream()`` remain owned by the caller and are -not destroyed. +will have no-ops for their respective cleanup functions. Cleanup invalidates every existing resource that refers to a Camp-managed stream. Applications must finish using those resources and ensure that no @@ -203,4 +200,4 @@ recreate the managed streams, but it does not make an older resource valid again. .. note:: - The ``cleanup()`` function is not thread-safe. + All ``cleanup()`` functions are not thread-safe. From db006876323b35a7ebe8f09f99f6eda0df80cf6f Mon Sep 17 00:00:00 2001 From: Brandon Echols Date: Tue, 25 Aug 2026 16:45:42 -0700 Subject: [PATCH 4/5] Adds a resettable once flag and call_once with resettable flag --- include/camp/init_helpers.hpp | 87 ++++++++++++++++++++++++++++++++++ include/camp/resource/cuda.hpp | 28 ++++++----- include/camp/resource/hip.hpp | 27 +++++++---- 3 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 include/camp/init_helpers.hpp diff --git a/include/camp/init_helpers.hpp b/include/camp/init_helpers.hpp new file mode 100644 index 00000000..01a220b3 --- /dev/null +++ b/include/camp/init_helpers.hpp @@ -0,0 +1,87 @@ +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// +// Copyright (c) Lawrence Livermore National Security, LLC and other +// Camp Project Developers. See top-level LICENSE and COPYRIGHT +// files for dates and other details. No copyright assignment is required +// to contribute to Camp. +// +// SPDX-License-Identifier: (BSD-3-Clause) +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// + +#ifndef CAMP_INIT_HELPERS_HPP +#define CAMP_INIT_HELPERS_HPP + +#include +#include +#include + +namespace camp +{ + +/// Resettable version of std::once_flag +/// +/// This is similar to std::once_flag used in std::call_once. However, +/// this version supports resetting flag, so that memory can be cleaned +/// and re-initialized at a later if needed. +/// +/// @note Currently, this uses atomic instead of atomic_flag to support +/// older versions of GCC. atomic_flag::test is not supported until +/// GCC 11. atomic_flag would be preferred as it is guaranteed to +/// not use a lock. +class resettable_once_flag +{ +public: + resettable_once_flag() : m_lock{}, m_flag{false} + {} + + resettable_once_flag(const resettable_once_flag&) = delete; + resettable_once_flag& operator=(const resettable_once_flag&) = delete; + + resettable_once_flag(resettable_once_flag&&) = delete; + resettable_once_flag& operator=(resettable_once_flag&&) = delete; + + bool test(std::memory_order order = std::memory_order_seq_cst) noexcept + { + return m_flag.load(order); + } + + void set(bool value, std::memory_order order = std::memory_order_seq_cst) noexcept + { + m_flag.store(value, order); + } + + void clear() + { + m_flag.store(false, std::memory_order_release); + } + + template + friend void call_once(camp::resettable_once_flag& flag, Callable&& callable, + Args&&... args); +private: + std::mutex m_lock; + std::atomic m_flag; +}; + +/// Resettable version of std::call_once +/// +/// This is similar to std::call_once. However, this version supports +/// uses a custom ``camp::resettable_once_flag`` that allows the +/// data to be cleaned up. If needed, calling this function again after +/// resetting the flag will call the callable function again. +template +void call_once(camp::resettable_once_flag& flag, Callable&& callable, Args&&... args) +{ + if (flag.test(std::memory_order::acquire)) [[likely]] { + return; + } + + std::lock_guard guard(flag.m_lock); + if (!flag.test(std::memory_order::relaxed)) { + callable(std::forward(args)...); + flag.set(true, std::memory_order::release); + } +} + +} // namespace camp + +#endif // CAMP_INIT_HELPERS_HPP diff --git a/include/camp/resource/cuda.hpp b/include/camp/resource/cuda.hpp index aa4840ae..2ce5dcd8 100644 --- a/include/camp/resource/cuda.hpp +++ b/include/camp/resource/cuda.hpp @@ -23,6 +23,7 @@ #include "camp/defines.hpp" #include "camp/helpers.hpp" +#include "camp/init_helpers.hpp" #include "camp/resource/event.hpp" #include "camp/resource/platform.hpp" @@ -158,7 +159,7 @@ namespace resources std::array streams{}; cudaStream_t default_stream = nullptr; int previous = num_streams - 1; - std::mutex mutex; + camp::resettable_once_flag flag; }; static stream_state& get_stream_state() @@ -170,13 +171,14 @@ namespace resources static cudaStream_t get_a_stream(int num) { auto& state = get_stream_state(); - std::lock_guard lock(state.mutex); - for (auto& s : state.streams) { - if (s == nullptr) { - CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, &s); + camp::call_once(state.flag, [&] () { + for (auto& s : state.streams) { + if (s == nullptr) { + CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, &s); + } } - } + }); if (num < 0) { state.previous = (state.previous + 1) % num_streams; @@ -241,11 +243,13 @@ namespace resources return Cuda(nullptr); #else auto& state = get_stream_state(); - std::lock_guard lock(state.mutex); - if (state.default_stream == nullptr) { - CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, - &state.default_stream); - } + + camp::call_once(state.flag, [&] () { + if (state.default_stream == nullptr) { + CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, + &state.default_stream); + } + }); return Cuda(state.default_stream); #endif } @@ -281,6 +285,8 @@ namespace resources state.default_stream = nullptr; } #endif + + state.flag.clear(); } CudaEvent get_event() diff --git a/include/camp/resource/hip.hpp b/include/camp/resource/hip.hpp index 55159b93..470df2ff 100644 --- a/include/camp/resource/hip.hpp +++ b/include/camp/resource/hip.hpp @@ -23,6 +23,7 @@ #include "camp/defines.hpp" #include "camp/helpers.hpp" +#include "camp/init_helpers.hpp" #include "camp/resource/event.hpp" #include "camp/resource/platform.hpp" @@ -159,7 +160,7 @@ namespace resources std::array streams{}; hipStream_t default_stream = nullptr; int previous = num_streams - 1; - std::mutex mutex; + camp::resettable_once_flag flag; }; static stream_state& get_stream_state() @@ -171,13 +172,14 @@ namespace resources static hipStream_t get_a_stream(int num) { auto& state = get_stream_state(); - std::lock_guard lock(state.mutex); - for (auto& s : state.streams) { - if (s == nullptr) { - CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &s); + camp::call_once(state.flag, [&] () { + for (auto& s : state.streams) { + if (s == nullptr) { + CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &s); + } } - } + }); if (num < 0) { state.previous = (state.previous + 1) % num_streams; @@ -239,10 +241,13 @@ namespace resources return Hip(nullptr); #else auto& state = get_stream_state(); - std::lock_guard lock(state.mutex); - if (state.default_stream == nullptr) { - CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &state.default_stream); - } + + camp::call_once(state.flag, [&] () { + if (state.default_stream == nullptr) { + CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &state.default_stream); + } + }); + return Hip(state.default_stream); #endif } @@ -277,6 +282,8 @@ namespace resources state.default_stream = nullptr; } #endif + + state.flag.clear(); } HipEvent get_event() From 300af671fca4c4a65cea01c8f9a8901bab7f5527 Mon Sep 17 00:00:00 2001 From: Brandon Echols Date: Tue, 25 Aug 2026 16:46:20 -0700 Subject: [PATCH 5/5] Adds unit tests for resettable once flag --- test/CMakeLists.txt | 1 + test/init_helpers.cpp | 68 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 test/init_helpers.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 63b2782d..54915114 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -71,6 +71,7 @@ camp_add_test(filter) camp_add_test(find_if) camp_add_test(flatten) camp_add_test(index_of) +camp_add_test(init_helpers) camp_add_test(lambda) camp_add_test(number) camp_add_test(prepend) diff --git a/test/init_helpers.cpp b/test/init_helpers.cpp new file mode 100644 index 00000000..a3e18bc3 --- /dev/null +++ b/test/init_helpers.cpp @@ -0,0 +1,68 @@ +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// +// Copyright (c) Lawrence Livermore National Security, LLC and other +// Camp Project Developers. See top-level LICENSE and COPYRIGHT +// files for dates and other details. No copyright assignment is required +// to contribute to Camp. +// +// SPDX-License-Identifier: (BSD-3-Clause) +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// + +#include + +#include "camp/camp.hpp" +#include "camp/init_helpers.hpp" +#include "gtest/gtest.h" + +TEST(CampInitHelpers, SimpleCallOnce) +{ + int test = 0; + camp::resettable_once_flag flag; + + camp::call_once(flag, [&] () { test += 1; }); + ASSERT_EQ(test, 1); + + camp::call_once(flag, [&] () { test += 1; }); + ASSERT_EQ(test, 1); + + flag.clear(); + camp::call_once(flag, [&] () { test += 1; }); + ASSERT_EQ(test, 2); +} + +TEST(CampInitHelpers, ThreadedCallOnce) +{ + int test = 0; + camp::resettable_once_flag flag; + + auto add_num_once = [&] (int i) { + camp::call_once(flag, [&test] (int j) { + // Ensures operation won't be too quick to test contention for + // lock + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + test += j; + }, i); + }; + + std::thread t1(add_num_once, 1); + std::thread t2(add_num_once, 1); + std::thread t3(add_num_once, 1); + std::thread t4(add_num_once, 1); + t1.join(); + t2.join(); + t3.join(); + t4.join(); + + ASSERT_EQ(test, 1); + + flag.clear(); + const int num = 5; + std::thread t5(add_num_once, num); + std::thread t6(add_num_once, num); + std::thread t7(add_num_once, num); + std::thread t8(add_num_once, num); + t5.join(); + t6.join(); + t7.join(); + t8.join(); + ASSERT_EQ(test, num+1); +}