Skip to content
Merged
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
4 changes: 4 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,13 @@ if(BUILD_CUDA AND BUILD_TBB)
if(BUILD_STDEXEC)
add_executable(exec_reco_stdexec exec_reco.cpp)
target_link_libraries(exec_reco_stdexec PRIVATE CoroutineTests CUDA::cudart TBB::tbb STDEXEC::stdexec)
add_executable(exec_delegate_stdexec exec_delegate.cpp)
target_link_libraries(exec_delegate_stdexec PRIVATE CoroutineTests CUDA::cudart TBB::tbb STDEXEC::stdexec)
endif()
if(BUILD_CAPY)
add_executable(capy_reco capy_reco.cpp)
target_link_libraries(capy_reco PRIVATE CoroutineTests CUDA::cudart TBB::tbb Boost::capy)
add_executable(capy_delegate capy_delegate.cpp)
target_link_libraries(capy_delegate PRIVATE CoroutineTests CUDA::cudart TBB::tbb Boost::capy)
endif()
endif()
6 changes: 6 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,9 @@ This is a variant of ["Exec tbb" example](#exec-tbb) using Boost.Capy and IoAwai
Link: [alien_reco.cpp](alien_reco.cpp), [exec_reco.cpp](exec_reco.cpp), [capy_reco.cpp](capy_reco.cpp)

These examples show a setup and coroutine chain loosely inspired by track reconstruction on GPU in high energy physics experiments. The `reconstruct` coroutine prepares a mockup input data on a CUDA device, then `co_await`s the `clustering` coroutine, then receives output from it and `co_await`s the `seeding` coroutine. Both `clustering` and `seeding` coroutines receive a device buffer, copy it asynchronously back to host, suspend until the copy is done, then count non-zero elements and allocate new buffer of that size for their results. In `main` the `reconstruct` coroutines are executed in a TBB task arena either synchronously waiting for the result from the submitting or dynamically starting a few `reconstruct` coroutines and waiting until all the work is finished. In the `alien_reco` the application is implemented with "alien" coroutines (as in [alien examples](#alien)), in `exec_reco_stdexec` the application is implemented with C++26 execution, in `capy_reco` the implementation uses Boost.Capy and IoAwaitables protocol ([p4003](https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2026/p4003r0.pdf)).

## Delegation

Link: [exec_delegation.cpp](exec_delegation.cpp), [capy_delegation.cpp](capy_delegation.cpp)

These examples are a modification of [reconstuction examples](#reconstruction) to delegate all the calls to CUDA API that are happening inside the task to execute on a designated thread by changing scheduler/executor. This represents a specific optimization: invoking CUDA APIs from multiple threads can incur performance penalties due to contention on internal CUDA locks. By delegating these calls to a single thread, this contention can be reduced. The examples demonstrate how this optimization can be implemented using coroutines schedulers/executors.
256 changes: 256 additions & 0 deletions examples/capy_delegate.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
#include <cuda_runtime_api.h>
#include <tbb/task_arena.h>

#include <boost/capy.hpp>
#include <boost/capy/ex/thread_pool.hpp>
#include <cstddef>
#include <iostream>
#include <latch>
#include <string_view>
#include <utility>

#include "capy_stream_await.hpp" // StreamIoAwaitable
#include "capy_task_arena_executor.hpp" // TaskArenaExecutor
#include "logging_utils.hpp" // log, format_name
#include "statuscode.hpp" // StatusCodeImpl

namespace tools {
struct Tag {
static constexpr const char* name = "tools";
};
using StatusCode = StatusCodeImpl<Tag>;
} // namespace tools

#define ERROR_CHECK_CUDA(EXP) \
do { \
cudaError_t errorCode = EXP; \
if (errorCode != cudaSuccess) { \
throw std::runtime_error( \
std::format("CUDA error at {}:{}: {}", __FILE__, __LINE__, \
cudaGetErrorString(errorCode))); \
} \
} while (false)

template <typename T>
struct DeviceBuffer {
T* ptr = nullptr;
std::size_t size = 0;
};

template <typename F>
boost::capy::task<void> coro_wrapper(F&& f) {
std::forward<F>(f)();
co_return;
}

boost::capy::task<DeviceBuffer<int>> clusterization(
DeviceBuffer<int> cells, cudaStream_t stream,
boost::capy::thread_pool& delegation_thread, std::string_view parent) {

const auto self = format_name(parent, "clusterization");
log(self) << "Starting clusterization" << std::endl;

const auto nCells = static_cast<int>(cells.size);

// Copy cells back to host to count non-zero entries
auto h_cells = std::vector<int>(nCells);

co_await boost::capy::run(
delegation_thread.get_executor())(coro_wrapper([&]() {
log(self) << "Delegated copy of cells from device to host" << std::endl;
ERROR_CHECK_CUDA(cudaMemcpyAsync(h_cells.data(), cells.ptr,
nCells * sizeof(int),
cudaMemcpyDeviceToHost, stream));
}));

ERROR_CHECK_CUDA(co_await StreamIoAwaitable{stream});

auto nClusters = 0;
for (auto v : h_cells)
if (v != 0)
++nClusters;

log(self) << "Found " << nClusters << " clusters" << std::endl;

// Allocate clusters of appropiate size on device
int* d_clusters = nullptr;

co_await boost::capy::run(
delegation_thread.get_executor())(coro_wrapper([&]() {
log(self) << "Delegated allocation of clusters on device" << std::endl;
ERROR_CHECK_CUDA(cudaMallocAsync(reinterpret_cast<void**>(&d_clusters),
nClusters * sizeof(int), stream));

// Write some dummy data to the clusters buffer to simulate work
ERROR_CHECK_CUDA(
cudaMemsetAsync(d_clusters, 0, nClusters * sizeof(int), stream));
ERROR_CHECK_CUDA(cudaMemsetAsync(d_clusters, 1,
nClusters / 2 * sizeof(int), stream));
}));

co_return DeviceBuffer<int>{d_clusters,
static_cast<std::size_t>(nClusters)};
}

boost::capy::task<DeviceBuffer<int>> seeding(
DeviceBuffer<int> clusters, cudaStream_t stream,
boost::capy::thread_pool& delegation_thread, std::string_view parent) {

const auto self = format_name(parent, "seeding");
log(self) << "Starting seeding" << std::endl;

const auto nClusters = static_cast<int>(clusters.size);

// Copy clusters to host to count non-zero entries
auto h_clusters = std::vector<int>(nClusters);

co_await boost::capy::run(delegation_thread.get_executor())(
coro_wrapper([&]() {
log(self) << "Delegated copy of clusters to host" << std::endl;
ERROR_CHECK_CUDA(cudaMemcpyAsync(h_clusters.data(), clusters.ptr,
nClusters * sizeof(int),
cudaMemcpyDeviceToHost, stream));
}));

ERROR_CHECK_CUDA(co_await StreamIoAwaitable{stream});

int nSeeds = 0;
for (auto v : h_clusters)
if (v != 0)
++nSeeds;

log(self) << "Found " << nSeeds << " seeds" << std::endl;

// Allocate seeds of appropiate size on device
int* d_seeds = nullptr;

co_await boost::capy::run(delegation_thread.get_executor())(
coro_wrapper([&]() {
log(self) << "Delegated allocation of seeds on device" << std::endl;
ERROR_CHECK_CUDA(cudaMallocAsync(reinterpret_cast<void**>(&d_seeds),
nSeeds * sizeof(int), stream));

// Write some dummy data to the seeds buffer to simulate work
ERROR_CHECK_CUDA(
cudaMemsetAsync(d_seeds, 0, nSeeds * sizeof(int), stream));
ERROR_CHECK_CUDA(
cudaMemsetAsync(d_seeds, 1, nSeeds / 2 * sizeof(int), stream));
}));

co_return DeviceBuffer<int>{d_seeds, static_cast<std::size_t>(nSeeds)};
}

boost::capy::task<tools::StatusCode> reconstruct(
cudaStream_t stream, boost::capy::thread_pool& delegation_thread,
std::string parent) {
const auto self = format_name(parent, "reconstruction");
log(self) << "Starting reconstruction" << std::endl;

// Allocate some dummy input data on the device
auto cells = DeviceBuffer<int>{nullptr, 1000};
co_await boost::capy::run(
delegation_thread.get_executor())(coro_wrapper([&]() {
log(self) << "Delegated allocation of input data on device"
<< std::endl;
ERROR_CHECK_CUDA(cudaMallocAsync(reinterpret_cast<void**>(&cells.ptr),
cells.size * sizeof(int), stream));
ERROR_CHECK_CUDA(
cudaMemsetAsync(cells.ptr, 1, cells.size * sizeof(int), stream));
}));

// Run the clusterization and seeding steps
auto clusters =
co_await clusterization(cells, stream, delegation_thread, self);
auto seeds = co_await seeding(clusters, stream, delegation_thread, self);

// Cleanup
co_await boost::capy::run(delegation_thread.get_executor())(
coro_wrapper([&]() {
log(self) << "Delegated cleanup of device memory" << std::endl;
ERROR_CHECK_CUDA(cudaFreeAsync(cells.ptr, stream));
ERROR_CHECK_CUDA(cudaFreeAsync(clusters.ptr, stream));
ERROR_CHECK_CUDA(cudaFreeAsync(seeds.ptr, stream));
}));

ERROR_CHECK_CUDA(co_await StreamIoAwaitable{stream});

log(self) << "Finishing reconstruction" << std::endl;
co_return tools::StatusCode::SUCCESS;
}

int main() {
int deviceCount = 0;
auto error_id = cudaGetDeviceCount(&deviceCount);

if (error_id != cudaSuccess) {
std::cout << "cudaGetDeviceCount returned "
<< static_cast<int>(error_id) << "\n"
<< cudaGetErrorString(error_id) << "\n";
return EXIT_FAILURE;
}

if (deviceCount == 0) {
std::cout << "No CUDA devices found.\n";
return EXIT_FAILURE;
}

log() << "main Starting" << std::endl;

auto task_arena = tbb::task_arena{2, 0};
auto context = TaskArenaContext(task_arena);
auto executor = TaskArenaExecutor(context);
auto delegation_thread = boost::capy::thread_pool(1);

{
std::cout << "--- Single event, synchronous wait for completion ---\n";
cudaStream_t stream;
ERROR_CHECK_CUDA(cudaStreamCreate(&stream));
auto final_result = tools::StatusCode{};
auto done = std::latch{1};
auto result_handler = [&done, &final_result](tools::StatusCode code) {
final_result = code;
done.count_down();
};

log() << "main Launching algorithm..." << std::endl;
boost::capy::run_async(executor, result_handler)(
reconstruct(stream, delegation_thread, "main"));
done.wait();
log() << "main Final status of algorithm " << final_result << ""
<< std::endl;
ERROR_CHECK_CUDA(cudaStreamDestroy(stream));
}

{
std::cout << "--- Multiple events, wait for all to complete ---\n ";

auto streams = std::vector<cudaStream_t>(2);
auto status = std::vector<tools::StatusCode>(streams.size());
for (auto& stream : streams) {
ERROR_CHECK_CUDA(cudaStreamCreate(&stream));
}

auto done = std::latch{static_cast<std::ptrdiff_t>(streams.size())};
log() << "main Launching algorithms..." << std::endl;

for (std::size_t i = 0; i < streams.size(); ++i) {
auto result_handler = [&done, &status, i](tools::StatusCode code) {
status.at(i) = code;
done.count_down();
};
boost::capy::run_async(executor, result_handler)(reconstruct(
streams.at(i), delegation_thread, "event" + std::to_string(i)));
}
done.wait();
for (auto& stream : streams) {
ERROR_CHECK_CUDA(cudaStreamDestroy(stream));
}

log() << "main All algorithms completed. Final statuses: ";
for (auto s : status) {
std::cout << s << ' ';
}
std::cout << std::endl;
}
return 0;
}
Loading