diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c66df80..70230fb 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -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() diff --git a/examples/README.md b/examples/README.md index 1d9ce3e..a8c3d90 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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. diff --git a/examples/capy_delegate.cpp b/examples/capy_delegate.cpp new file mode 100644 index 0000000..b8236ca --- /dev/null +++ b/examples/capy_delegate.cpp @@ -0,0 +1,256 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#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; +} // 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 +struct DeviceBuffer { + T* ptr = nullptr; + std::size_t size = 0; +}; + +template +boost::capy::task coro_wrapper(F&& f) { + std::forward(f)(); + co_return; +} + +boost::capy::task> clusterization( + DeviceBuffer 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(cells.size); + + // Copy cells back to host to count non-zero entries + auto h_cells = std::vector(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(&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{d_clusters, + static_cast(nClusters)}; +} + +boost::capy::task> seeding( + DeviceBuffer 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(clusters.size); + + // Copy clusters to host to count non-zero entries + auto h_clusters = std::vector(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(&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{d_seeds, static_cast(nSeeds)}; +} + +boost::capy::task 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{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(&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(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(2); + auto status = std::vector(streams.size()); + for (auto& stream : streams) { + ERROR_CHECK_CUDA(cudaStreamCreate(&stream)); + } + + auto done = std::latch{static_cast(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; +} diff --git a/examples/exec_delegate.cpp b/examples/exec_delegate.cpp new file mode 100644 index 0000000..8c5a40a --- /dev/null +++ b/examples/exec_delegate.cpp @@ -0,0 +1,263 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "exec_stream_await_sender.hpp" // stream_await_sender +#include "exec_task_arena_scheduler.hpp" // TaskArenaScheduler +#include "logging_utils.hpp" // log, format_name +#include "statuscode.hpp" // StatusCodeImpl + +namespace tools { +struct Tag { + static constexpr const char* name = "tools"; +}; +using StatusCode = StatusCodeImpl; +} // 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 +struct DeviceBuffer { + T* ptr = nullptr; + std::size_t size = 0; +}; + +exec::task> clusterization( + DeviceBuffer cells, cudaStream_t stream, + exec::single_thread_context& delegation_ctx, std::string_view parent) { + + const auto self = format_name(parent, "clusterization"); + log(self) << "Starting clusterization" << std::endl; + + const auto nCells = static_cast(cells.size); + + // Copy cells back to host to count non-zero entries + auto h_cells = std::vector(nCells); + + stdexec::sender auto copy_cells = + stdexec::just() | stdexec::then([&]() { + 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)); + }); + co_await stdexec::on(delegation_ctx.get_scheduler(), std::move(copy_cells)); + + ERROR_CHECK_CUDA(co_await stream_await_sender{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; + + stdexec::sender auto allocate_clusters = + stdexec::just() | stdexec::then([&]() { + log(self) << "Delegated allocation of clusters on device" + << std::endl; + ERROR_CHECK_CUDA( + cudaMallocAsync(reinterpret_cast(&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_await stdexec::on(delegation_ctx.get_scheduler(), + std::move(allocate_clusters)); + + co_return DeviceBuffer{d_clusters, + static_cast(nClusters)}; +} + +exec::task> seeding( + DeviceBuffer clusters, cudaStream_t stream, + exec::single_thread_context& delegation_ctx, std::string_view parent) { + + const auto self = format_name(parent, "seeding"); + log(self) << "Starting seeding" << std::endl; + + const auto nClusters = static_cast(clusters.size); + + // Copy clusters to host to count non-zero entries + auto h_clusters = std::vector(nClusters); + + stdexec::sender auto copy_clusters = + stdexec::just() | stdexec::then([&]() { + log(self) << "Delegated copy of clusters from device to host" + << std::endl; + ERROR_CHECK_CUDA(cudaMemcpyAsync(h_clusters.data(), clusters.ptr, + nClusters * sizeof(int), + cudaMemcpyDeviceToHost, stream)); + }); + co_await stdexec::on(delegation_ctx.get_scheduler(), + std::move(copy_clusters)); + + ERROR_CHECK_CUDA(co_await stream_await_sender{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; + + stdexec::sender auto allocate_seeds = + stdexec::just() | stdexec::then([&]() { + log(self) << "Delegated allocation of seeds on device" << std::endl; + ERROR_CHECK_CUDA(cudaMallocAsync(reinterpret_cast(&d_seeds), + nSeeds * sizeof(int), stream)); + + // Write some dummy data to the clusters 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_await stdexec::on(delegation_ctx.get_scheduler(), + std::move(allocate_seeds)); + + co_return DeviceBuffer{d_seeds, static_cast(nSeeds)}; +} + +exec::task reconstruct( + cudaStream_t stream, exec::single_thread_context& delegation_ctx, + std::string_view 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{nullptr, 1000}; + stdexec::sender auto allocate_cells = + stdexec::just() | stdexec::then([&]() { + log(self) << "Delegated allocation of input data on device" + << std::endl; + ERROR_CHECK_CUDA( + cudaMallocAsync(reinterpret_cast(&cells.ptr), + cells.size * sizeof(int), stream)); + ERROR_CHECK_CUDA(cudaMemsetAsync(cells.ptr, 1, + cells.size * sizeof(int), stream)); + }); + co_await stdexec::on(delegation_ctx.get_scheduler(), + std::move(allocate_cells)); + + // Run the clusterization and seeding steps + auto clusters = + co_await clusterization(cells, stream, delegation_ctx, self); + auto seeds = co_await seeding(clusters, stream, delegation_ctx, self); + + // Cleanup + stdexec::sender auto cleanup = + stdexec::just() | stdexec::then([&]() { + 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)); + }); + co_await stdexec::on(delegation_ctx.get_scheduler(), std::move(cleanup)); + + ERROR_CHECK_CUDA(co_await stream_await_sender{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(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; + + tbb::task_arena task_arena{2, 0}; + execution::scheduler auto scheduler = get_scheduler(task_arena, false); + exec::single_thread_context delegation_context; + + { + cudaStream_t stream; + ERROR_CHECK_CUDA(cudaStreamCreate(&stream)); + std::cout << "--- Single event, synchronous wait for completion ---\n"; + log() << "main Launching algorithm..." << std::endl; + auto [status] = + stdexec::sync_wait( + stdexec::starts_on( + scheduler, reconstruct(stream, delegation_context, "main"))) + .value(); + log() << "main Final status of algorithm " << status << "" << std::endl; + ERROR_CHECK_CUDA(cudaStreamDestroy(stream)); + } + + { + std::cout << "--- Multiple events, wait for all to complete ---\n"; + + auto streams = std::vector(2); + auto status = std::vector(streams.size()); + for (auto& stream : streams) { + ERROR_CHECK_CUDA(cudaStreamCreate(&stream)); + } + + auto scope = exec::async_scope{}; + log() << "main Launching algorithms..." << std::endl; + + auto payload = [](std::vector streams, + std::vector& statuses, int i, + exec::single_thread_context& delegation_ctx) + -> exec::task { + const auto name = std::format("event{}:", i); + auto& stream = streams.at(i); + auto& status = statuses.at(i); + status = co_await reconstruct(stream, delegation_ctx, name); + }; + for (std::size_t i = 0; i < streams.size(); ++i) { + scope.spawn(stdexec::starts_on( + scheduler, payload(streams, status, i, delegation_context))); + } + stdexec::sync_wait(scope.on_empty()); + + 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; +} diff --git a/extern/capy/CMakeLists.txt b/extern/capy/CMakeLists.txt index 7761a5a..e0e324d 100644 --- a/extern/capy/CMakeLists.txt +++ b/extern/capy/CMakeLists.txt @@ -6,7 +6,7 @@ message(STATUS "Building capy as part of the CoroutineTests project") # Declare where to get stdexec from. set(COROUTINETESTS_CAPY_SOURCE - "URL;https://github.com/cppalliance/capy/archive/df92ffbb0670c06ebf0f201da73f06d1385d5d9b.zip;URL_MD5;d6b6e1e25684986d17ae92c13f992cfc" + "URL;https://github.com/cppalliance/capy/archive/e2de31c559b60df471e2d80c2b3e8373e2fc14e8.zip;URL_MD5;a5d719eb6b234daaab69a62c9ff23a01" CACHE STRING "Source for capy, when built as part of this project") mark_as_advanced(COROUTINETESTS_CAPY_SOURCE)