Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
08dd7f9
Implement event pool
kingcrimsontianyu Feb 1, 2026
3948400
Fix stream race condition
kingcrimsontianyu Feb 2, 2026
43a38d9
Make ctor dtor private. Remove inner NVTX
kingcrimsontianyu Feb 2, 2026
bf20744
Initial impl of event pool
kingcrimsontianyu Feb 2, 2026
7e0c71f
Set a get() overload to private
kingcrimsontianyu Feb 2, 2026
74e6740
Add Doxygen comments
kingcrimsontianyu Feb 2, 2026
812c8a9
Remove get(ctx, tid) and move its content to get()
kingcrimsontianyu Feb 3, 2026
c9d7acd
Merge branch 'fix-stream-bug' into event-pool
kingcrimsontianyu Feb 3, 2026
d878f79
Log exception msg
kingcrimsontianyu Feb 3, 2026
df8a4a8
Update cpp/src/detail/event.cpp
kingcrimsontianyu Feb 3, 2026
9d73727
Update name for clarity
kingcrimsontianyu Feb 3, 2026
b71c857
Update
kingcrimsontianyu Feb 3, 2026
dab118e
Update
kingcrimsontianyu Feb 3, 2026
3103863
Silly bug fixes
kingcrimsontianyu Feb 4, 2026
4208e5c
Update
kingcrimsontianyu Feb 4, 2026
badc794
Merge branch 'main' into event-pool
kingcrimsontianyu Feb 5, 2026
075dbd8
Merge branch 'main' into event-pool
kingcrimsontianyu Feb 9, 2026
64ee425
Add event query
kingcrimsontianyu Feb 9, 2026
87e8fe2
Merge branch 'main' into event-pool
kingcrimsontianyu Feb 13, 2026
e741a51
Merge branch 'main' into event-pool
kingcrimsontianyu May 19, 2026
c323302
Merge branch 'main' into event-pool
kingcrimsontianyu May 21, 2026
a97196b
Improve event pool impl. Add tests
kingcrimsontianyu May 22, 2026
4f820a3
Add cuCtxCreate/Destroy to shim to improve event pool testing
kingcrimsontianyu May 22, 2026
d1609a0
Improve naming a bit
kingcrimsontianyu May 23, 2026
ad87ea0
Rename Event* to CudaEvent*
kingcrimsontianyu May 26, 2026
5d22b95
Update doc for event is_done method
kingcrimsontianyu May 26, 2026
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
4 changes: 3 additions & 1 deletion cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# =============================================================================
# cmake-format: off
# SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0
# cmake-format: on
# =============================================================================
Expand Down Expand Up @@ -153,8 +153,10 @@ set(SOURCES
"src/file_utils.cpp"
"src/mmap.cpp"
"src/detail/env.cpp"
"src/detail/event.cpp"
"src/detail/nvtx.cpp"
"src/detail/posix_io.cpp"
"src/detail/stream.cpp"
"src/shim/cuda.cpp"
"src/shim/cufile.cpp"
"src/shim/utils.cpp"
Expand Down
152 changes: 152 additions & 0 deletions cpp/include/kvikio/detail/event.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once

#include <mutex>
#include <unordered_map>
#include <vector>

#include <kvikio/shim/cuda.hpp>

namespace kvikio::detail {
/**
* @brief Thread-safe singleton pool for reusable CUDA events
*
* Manages a pool of CUDA events organized by CUDA context. Events are retained and reused across
* calls to minimize allocation overhead. Each context maintains its own separate pool of events
* since CUDA events are context-specific resources.
*
* All events are created with `CU_EVENT_DISABLE_TIMING` for minimal overhead.
*
* Call `EventPool::instance().get()` to acquire an event that will be automatically returned to the
* pool when it goes out of scope (RAII).
*
* @note The destructor intentionally leaks events to avoid CUDA cleanup issues when static
* destructors run after CUDA context destruction. @sa BounceBufferPool
*/
class EventPool {

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.

Consider naming them CudaEventPool and CudaEvent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure! Done!

public:
class Event {
friend class EventPool;

private:
CUevent _event{nullptr};
CUcontext _context{nullptr};

/**
* @brief Construct an Event wrapping a CUDA event handle
*
* @param event The CUDA event handle to wrap
* @param context The CUDA context associated with this event
*/
explicit Event(CUevent event, CUcontext context) noexcept;

public:
~Event() noexcept;

// Move-only
Event(Event const&) = delete;
Event& operator=(Event const&) = delete;
Event(Event&& other) noexcept;
Event& operator=(Event&& other) noexcept;

/**
* @brief Get the underlying CUDA event handle
*
* @return The CUevent handle wrapped by this object
*/
[[nodiscard]] CUevent get() const noexcept;

/**
* @brief Get the CUDA context associated with this event
*
* @return The CUcontext this event belongs to
*/
[[nodiscard]] CUcontext context() const noexcept;

/**
* @brief Record the event on a CUDA stream
*
* Records the event to capture the current state of the stream. The event will be signaled when
* all preceding operations on the stream have completed.
*
* @param stream The CUDA stream to record the event on (must belong to the same context as this
* event)
*/
void record(CUstream stream);

/**
* @brief Block the calling thread until the event has been signaled
*
* Waits for all work captured by a preceding record() call to complete.
*
* @exception CudaError if the synchronize operation fails
*/
void synchronize();
};

private:
std::mutex mutable _mutex;
// Per-context pools of free events
std::unordered_map<CUcontext, std::vector<CUevent>> _pools;

EventPool() = default;

// Intentionally leak events during static destruction. @sa BounceBufferPool
~EventPool() noexcept = default;

public:
// Non-copyable, non-movable singleton
EventPool(EventPool const&) = delete;
EventPool& operator=(EventPool const&) = delete;
EventPool(EventPool&&) = delete;
EventPool& operator=(EventPool&&) = delete;

/**
* @brief Acquire a CUDA event from the pool
*
* Returns a cached event for the current CUDA context if available, otherwise creates a new one.
* The returned Event object will automatically return the event to the pool when it goes out of
* scope.
*
* @return RAII Event object wrapping the acquired CUDA event
* @exception CudaError if no CUDA context is current or event creation fails
*/
[[nodiscard]] Event get();

/**
* @brief Return an event to the pool for reuse
*
* Typically called automatically by Event's destructor. Adds the event to the pool associated
* with its context for future reuse.
*
* @param event The CUDA event handle to return
* @param context The CUDA context associated with the event
*/
void put(CUevent event, CUcontext context) noexcept;

/**
* @brief Get the number of free events for a specific context
*
* @param context The CUDA context to query
* @return The number of events available for reuse in that context's pool
*/
[[nodiscard]] std::size_t num_free_events(CUcontext context) const;

/**
* @brief Get the total number of free events across all contexts
*
* @return The total count of events available for reuse
*/
[[nodiscard]] std::size_t total_free_events() const;

/**
* @brief Get the singleton instance of the event pool
*
* @return Reference to the singleton EventPool instance
*/
static EventPool& instance();
};
} // namespace kvikio::detail
39 changes: 4 additions & 35 deletions cpp/include/kvikio/detail/posix_io.hpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once

#include <unistd.h>

#include <cstddef>
#include <cstdlib>
#include <map>
#include <thread>
#include <type_traits>

#include <kvikio/bounce_buffer.hpp>
#include <kvikio/detail/nvtx.hpp>
#include <kvikio/detail/stream.hpp>
#include <kvikio/detail/utils.hpp>
#include <kvikio/error.hpp>
#include <kvikio/shim/cuda.hpp>
Expand All @@ -36,37 +36,6 @@ enum class PartialIO : uint8_t {
NO, ///< POSIX read/write is called repeatedly until all requested bytes are processed.
};

/**
* @brief Singleton class to retrieve a CUDA stream for device-host copying
*
* Call `StreamsByThread::get` to get the CUDA stream assigned to the current
* CUDA context and thread.
*/
class StreamsByThread {
private:
std::map<std::pair<CUcontext, std::thread::id>, CUstream> _streams;

public:
StreamsByThread() = default;

// Here we intentionally do not destroy in the destructor the CUDA resources
// (e.g. CUstream) with static storage duration, but instead let them leak
// on program termination. This is to prevent undefined behavior in CUDA. See
// <https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#initialization>
// This also prevents crash (segmentation fault) if clients call
// cuDevicePrimaryCtxReset() or cudaDeviceReset() before program termination.
~StreamsByThread() = default;

KVIKIO_EXPORT static CUstream get(CUcontext ctx, std::thread::id thd_id);

static CUstream get();

StreamsByThread(StreamsByThread const&) = delete;
StreamsByThread& operator=(StreamsByThread const&) = delete;
StreamsByThread(StreamsByThread&& o) = delete;
StreamsByThread& operator=(StreamsByThread&& o) = delete;
};

/**
* @brief Read or write host memory to or from disk using POSIX with opportunistic Direct I/O
*
Expand Down Expand Up @@ -238,7 +207,7 @@ std::size_t posix_device_io(int fd_direct_off,
off_t const chunk_size2 = convert_size2off(bounce_buffer.size());

// Get a stream for the current CUDA context and thread
CUstream stream = StreamsByThread::get();
CUstream stream = StreamCachePerThreadAndContext::get();

while (bytes_remaining > 0) {
off_t const nbytes_requested = std::min(chunk_size2, bytes_remaining);
Expand Down
67 changes: 67 additions & 0 deletions cpp/include/kvikio/detail/stream.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once

#include <map>
#include <mutex>
#include <thread>

#include <kvikio/shim/cuda.hpp>

namespace kvikio::detail {
/**
* @brief Singleton cache that provides one CUDA stream per (context, thread) pair.
*
* This class manages CUDA streams used for host-device memory transfers. Each unique combination of
* CUDA context and calling thread is assigned a dedicated stream, which is created lazily on first
* access and reused for subsequent calls.
*
* The cache is thread-safe and handles concurrent access from multiple threads.
*
* @note CUDA streams are intentionally leaked on program termination rather than destroyed in the
* destructor. This avoids undefined behavior that can occur when destroying CUDA resources during
* static destruction, and prevents crashes (segmentation faults) if clients call
* cuDevicePrimaryCtxReset() or cudaDeviceReset() before program termination. See:
* https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#initialization
*/
class StreamCachePerThreadAndContext {
private:
std::map<std::pair<CUcontext, std::thread::id>, CUstream> _streams;
std::mutex mutable _mutex;

private:
StreamCachePerThreadAndContext() = default;
~StreamCachePerThreadAndContext() = default;

public:
/**
* @brief Get or create a CUDA stream for the specified context and thread.
*
* If a stream already exists for the given (context, thread) pair, it is returned. Otherwise, a
* new stream is created, cached, and returned.
*
* @param ctx The CUDA context. If null, the null stream is returned.
* @param thd_id The thread identifier.
* @return The CUDA stream associated with this (context, thread) pair, or nullptr if @p ctx is
* null.
*/
KVIKIO_EXPORT static CUstream get(CUcontext ctx, std::thread::id thd_id);

/**
* @brief Get or create a CUDA stream for the current context and thread.
*
* Convenience overload that uses the current CUDA context and calling thread's ID.
*
* @return The CUDA stream associated with the current (context, thread) pair, or nullptr if no
* CUDA context is current.
*/
static CUstream get();

StreamCachePerThreadAndContext(StreamCachePerThreadAndContext const&) = delete;
StreamCachePerThreadAndContext& operator=(StreamCachePerThreadAndContext const&) = delete;
StreamCachePerThreadAndContext(StreamCachePerThreadAndContext&& o) = delete;
StreamCachePerThreadAndContext& operator=(StreamCachePerThreadAndContext&& o) = delete;
};
} // namespace kvikio::detail
4 changes: 4 additions & 0 deletions cpp/include/kvikio/shim/cuda.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ class cudaAPI {
decltype(cuStreamCreate)* StreamCreate{nullptr};
decltype(cuStreamDestroy)* StreamDestroy{nullptr};
decltype(cuDriverGetVersion)* DriverGetVersion{nullptr};
decltype(cuEventSynchronize)* EventSynchronize{nullptr};
decltype(cuEventCreate)* EventCreate{nullptr};
decltype(cuEventDestroy)* EventDestroy{nullptr};
decltype(cuEventRecord)* EventRecord{nullptr};

private:
cudaAPI();
Expand Down
Loading