Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/sphinx/user_guide/feature/resource.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,21 @@ 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<Host, Value> map`` would only ever have one entry.

Cleaning Up Resources
^^^^^^^^^^^^^^^^^^^^^

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.

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.

.. note::
All ``cleanup()`` functions are not thread-safe.
87 changes: 87 additions & 0 deletions include/camp/init_helpers.hpp
Original file line number Diff line number Diff line change
@@ -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 <atomic>
#include <mutex>
#include <utility>

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<bool> 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 <typename Callable, typename... Args>
friend void call_once(camp::resettable_once_flag& flag, Callable&& callable,
Args&&... args);
private:
std::mutex m_lock;
std::atomic<bool> 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 <typename Callable, typename... Args>
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>(args)...);
flag.set(true, std::memory_order::release);
}
}

} // namespace camp

#endif // CAMP_INIT_HELPERS_HPP
35 changes: 35 additions & 0 deletions include/camp/resource.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,41 @@ struct hash<camp::resources::Resource> {
#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 */
93 changes: 72 additions & 21 deletions include/camp/resource/cuda.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -152,27 +153,39 @@ namespace resources

class Cuda
{
static constexpr int num_streams = 16;

struct stream_state {
std::array<cudaStream_t, num_streams> streams{};
cudaStream_t default_stream = nullptr;
int previous = num_streams - 1;
camp::resettable_once_flag flag;
};

static stream_state& get_stream_state()
{
static stream_state state;

@MrBurmark MrBurmark Aug 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we're manually handling the thread safety now, we should probably make this a class static member variable so it doesn't get another layer of thread safe initialization that comes from being a function local static. Given that we have a library we should probably put the variable definition in the camp sources, as opposed to an inline static member var.

return state;
}

static cudaStream_t get_a_stream(int num)
{
static constexpr int num_streams = 16;
static std::array<cudaStream_t, num_streams> s_streams = [] {
std::array<cudaStream_t, num_streams> streams;
for (auto& s : streams) {
CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, &s);
}
return streams;
}();
auto& state = get_stream_state();

static std::mutex s_mtx;
static int s_previous = num_streams - 1;
camp::call_once(state.flag, [&] () {
for (auto& s : state.streams) {
if (s == nullptr) {
CAMP_CUDA_API_INVOKE_AND_CHECK(cudaStreamCreate, &s);
}
}
});

if (num < 0) {
std::lock_guard<std::mutex> lock(s_mtx);
s_previous = (s_previous + 1) % num_streams;
return s_streams[s_previous];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be locked.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact does it make sense to make this whole function a member function of the state now?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably makes sense to make getting a stream and getting the default stream member functions at this point.

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
Expand Down Expand Up @@ -226,16 +239,54 @@ 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();

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);
Comment thread
MrBurmark marked this conversation as resolved.
#endif
return s;
}());
return c;
}

/**
* \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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make more sense to put most of this code in a member function of the state?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can move most of the cleanup logic to be a state member function. However, I think we will still want a static cleanup function on the resource to be consistent across resources.

{
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

state.flag.clear();
}

CudaEvent get_event()
Expand Down
92 changes: 71 additions & 21 deletions include/camp/resource/hip.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -153,27 +154,39 @@ namespace resources

class Hip
{
static constexpr int num_streams = 16;

struct stream_state {
std::array<hipStream_t, num_streams> streams{};
hipStream_t default_stream = nullptr;
int previous = num_streams - 1;
camp::resettable_once_flag flag;
};

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<hipStream_t, num_streams> s_streams = [] {
std::array<hipStream_t, num_streams> streams;
for (auto& s : streams) {
CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &s);
}
return streams;
}();
auto& state = get_stream_state();

static std::mutex s_mtx;
static int s_previous = num_streams - 1;
camp::call_once(state.flag, [&] () {
for (auto& s : state.streams) {
if (s == nullptr) {
CAMP_HIP_API_INVOKE_AND_CHECK(hipStreamCreate, &s);
}
}
});

if (num < 0) {
std::lock_guard<std::mutex> 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
Expand Down Expand Up @@ -224,16 +237,53 @@ 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();

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
}

/**
* \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;

state.flag.clear();
}

HipEvent get_event()
Expand Down
Loading
Loading