Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
tbb_cuda_scheduler.hpp
Go to the documentation of this file.
1
6#pragma once
7
8#include "cuda_task.hpp"
9#include "task_registry.hpp"
10#include "tbb_scheduler.hpp"
12
13#include <cuda_runtime_api.h>
14#include <oneapi/tbb/task_arena.h>
15#include <oneapi/tbb/task_group.h>
16#include <oneapi/tbb/task_scheduler_observer.h>
20#include <uni20/config.hpp>
21
22#include <algorithm>
23#include <memory>
24#include <optional>
25#include <utility>
26#include <vector>
27
28namespace uni20::async
29{
30
31namespace detail
32{
33
34[[noreturn]] inline void cuda_arena_observer_failure(cudaError_t status, char const* operation, int device)
35{
36 PANIC("CUDA arena observer operation failed", operation, device, cudaGetErrorName(status),
37 cudaGetErrorString(status));
38}
39
40inline void check_cuda_arena_observer(cudaError_t status, char const* operation, int device) noexcept
41{
42 if (status != cudaSuccess) cuda_arena_observer_failure(status, operation, device);
43}
44
46class CudaArenaObserver final : public oneapi::tbb::task_scheduler_observer {
47 public:
48 CudaArenaObserver(oneapi::tbb::task_arena& arena, int device)
49 : oneapi::tbb::task_scheduler_observer(arena), device_(device)
50 {}
51
52 CudaArenaObserver(CudaArenaObserver const&) = delete;
53 CudaArenaObserver& operator=(CudaArenaObserver const&) = delete;
54
55 ~CudaArenaObserver() override { this->stop(); }
56
57 void start() { this->observe(true); }
58
59 void stop() noexcept
60 {
61 if (this->is_observing()) this->observe(false);
62 }
63
64 private:
65 struct Selection
66 {
67 CudaArenaObserver const* observer;
68 int previous_device;
69 };
70
71 static std::vector<Selection>& selection_stack()
72 {
73 static thread_local std::vector<Selection> stack;
74 return stack;
75 }
76
77 void on_scheduler_entry(bool) noexcept override
78 {
79 int previous_device = -1;
80 check_cuda_arena_observer(cudaGetDevice(&previous_device), "cudaGetDevice on arena entry", device_);
81 selection_stack().push_back(Selection{this, previous_device});
82 check_cuda_arena_observer(cudaSetDevice(device_), "cudaSetDevice on arena entry", device_);
83 }
84
85 void on_scheduler_exit(bool) noexcept override
86 {
87 auto& stack = selection_stack();
88 CHECK(!stack.empty());
89 CHECK_EQUAL(stack.back().observer, this);
90 int const previous_device = stack.back().previous_device;
91 stack.pop_back();
92 check_cuda_arena_observer(cudaSetDevice(previous_device), "cudaSetDevice on arena exit", previous_device);
93 }
94
95 int device_;
96};
97
98inline void verify_tbb_cuda_device(int expected_device)
99{
100#if UNI20_ASYNC_DEBUG
101 int current_device = -1;
102 check_cuda_arena_observer(cudaGetDevice(&current_device), "cudaGetDevice before coroutine resume", expected_device);
103 DEBUG_CHECK_EQUAL(current_device, expected_device);
104#else
105 static_cast<void>(expected_device);
106#endif
107}
108
109} // namespace detail
110
113{
115 int host_max_concurrency = oneapi::tbb::task_arena::automatic;
122 std::optional<int> default_cuda_device{};
123};
124
130class TbbCudaScheduler final : public TbbScheduler, public ICudaScheduler {
131 public:
135 : TbbCudaScheduler(cuda::Device::enumerate(), std::move(options))
136 {}
137
141 explicit TbbCudaScheduler(std::vector<cuda::Device> devices, TbbCudaSchedulerOptions options = {})
142 : TbbScheduler(options.host_max_concurrency)
143 {
144 device_arenas_.reserve(devices.size());
145 for (auto device : devices)
146 {
147 CHECK(!this->find_device_arena(device.ordinal()), "CUDA device enrolled more than once", device.ordinal());
148 device_arenas_.push_back(std::make_unique<DeviceArena>(device, options.cuda_max_concurrency_per_device));
149 }
150
151 if (options.default_cuda_device)
152 {
153 default_device_ = this->device_arena(*options.default_cuda_device).device.ordinal();
154 }
155 else if (!device_arenas_.empty())
156 {
157 default_device_ = device_arenas_.front()->device.ordinal();
158 }
159 }
160
163
164 ~TbbCudaScheduler() noexcept override { this->wait_for_submitted_tasks(); }
165
167
170 void schedule(CudaTask&& task) override
171 {
172 CHECK(default_device_, "cannot schedule an unbound CUDA task without an enrolled default device");
173 TaskRegistry::record_task_scheduled(task.coroutine_handle());
174 if (task.set_scheduler(this)) this->enqueue_task(std::move(task));
175 }
176
180 void schedule(CudaTask&& task, int device) override
181 {
182 auto& device_arena = this->device_arena(device);
183 cuda_promise(task.handle()).bind_device(device_arena.device.ordinal());
184 TaskRegistry::record_task_scheduled(task.coroutine_handle());
185 if (task.set_scheduler(this)) this->enqueue_task(std::move(task));
186 }
187
189 [[nodiscard]] bool has_device(int device) const noexcept { return this->find_device_arena(device) != nullptr; }
190
191 private:
192 bool accepts_route(TaskRoute route) const noexcept override
193 {
194 if (route.domain == TaskDomain::host) return !route.cuda_device;
195 if (route.domain != TaskDomain::cuda) return false;
196 auto const device = route.cuda_device ? route.cuda_device : default_device_;
197 return device && this->has_device(*device);
198 }
199
200 class DeviceArena {
201 public:
202 DeviceArena(cuda::Device selected_device, int max_concurrency)
203 : device(selected_device), arena(max_concurrency, /*reserved_for_application_threads=*/0),
204 observer(arena, device.ordinal())
205 {
206 arena.initialize();
207 {
208 cuda::ScopedDevice device_scope(device.ordinal());
209 cuda::check(cudaSetDevice(device.ordinal()), "cudaSetDevice for TBB CUDA scheduler initialization",
210 device.ordinal());
211 }
212 observer.start();
213 }
214
215 ~DeviceArena() { observer.stop(); }
216
217 cuda::Device device;
218 oneapi::tbb::task_arena arena;
219 detail::CudaArenaObserver observer;
220 };
221
222 bool can_direct_transfer(TaskHandle from, TaskHandle to) const noexcept override
223 {
224 if (from.domain() != TaskDomain::cuda) return true;
225 auto const from_device = this->activation_device(from);
226 auto const to_device = this->activation_device(to);
227 return from_device && from_device == to_device && this->has_device(*from_device);
228 }
229
230 void dispatch_handle(TaskHandle handle) override
231 {
232 if (handle.domain() != TaskDomain::cuda)
233 {
235 return;
236 }
237
238 auto const device = this->activation_device(handle);
239 CHECK(device, "CUDA task has no affinity and the scheduler has no default CUDA device");
240 auto& selected = this->device_arena(*device);
241 this->dispatch_handle_in_arena(selected.arena, handle, {.scheduler = "TbbCudaScheduler", .device = *device},
242 [device = *device] { detail::verify_tbb_cuda_device(device); });
243 }
244
245 [[nodiscard]] DeviceArena* find_device_arena(int device) const noexcept
246 {
247 auto const found = std::find_if(device_arenas_.begin(), device_arenas_.end(), [device](auto const& candidate) {
248 return candidate->device.ordinal() == device;
249 });
250 return found == device_arenas_.end() ? nullptr : found->get();
251 }
252
253 DeviceArena& device_arena(int device)
254 {
255 auto* result = this->find_device_arena(device);
256 CHECK(result, "CUDA task submitted to a device not enrolled in this scheduler", device);
257 return *result;
258 }
259
260 [[nodiscard]] std::optional<int> activation_device(TaskHandle task) const noexcept
261 {
262 if (auto const affinity = cuda_promise(task).device()) return affinity;
263 return default_device_;
264 }
265
266 std::vector<std::unique_ptr<DeviceArena>> device_arenas_;
267 std::optional<int> default_device_{};
268};
269
270} // namespace uni20::async
static void record_task_scheduled(std::coroutine_handle<> h)
Records the call stack where a coroutine was submitted to a scheduler.
void bind_device(int device)
Bind the CUDA device before the coroutine starts.
Definition async_task_promise.hpp:673
CUDA-constrained public task type used for typed initial admission.
Definition cuda_task.hpp:14
Scheduler interface for initial CudaTask admission.
Definition cuda_task.hpp:49
Unified oneTBB scheduler with one host arena and one arena per CUDA device.
Definition tbb_cuda_scheduler.hpp:130
bool has_device(int device) const noexcept
Report whether a CUDA device has an execution arena in this scheduler.
Definition tbb_cuda_scheduler.hpp:189
TbbCudaScheduler(TbbCudaScheduler const &)=delete
void schedule(CudaTask &&task, int device) override
Bind and submit a CUDA task to an enrolled device arena.
Definition tbb_cuda_scheduler.hpp:180
void schedule(CudaTask &&task) override
Submit a CUDA task without establishing device affinity.
Definition tbb_cuda_scheduler.hpp:170
TbbCudaScheduler & operator=(TbbCudaScheduler const &)=delete
~TbbCudaScheduler() noexcept override
Definition tbb_cuda_scheduler.hpp:164
TbbCudaScheduler(std::vector< cuda::Device > devices, TbbCudaSchedulerOptions options={})
Construct a scheduler for an explicit set of CUDA devices.
Definition tbb_cuda_scheduler.hpp:141
TbbCudaScheduler(TbbCudaSchedulerOptions options={})
Construct a scheduler enrolling every visible CUDA device.
Definition tbb_cuda_scheduler.hpp:134
Definition tbb_scheduler.hpp:59
void dispatch_handle_in_arena(oneapi::tbb::task_arena &arena, TaskHandle handle, detail::TbbTaskAdmissionContext context, Prepare &&prepare)
Submit one activation to an arena through the shared task group.
Definition tbb_scheduler.hpp:320
void enqueue_task(BasicTask &&t)
Enqueue one task through the scheduler's domain-routing hook.
Definition tbb_scheduler.hpp:281
void wait_for_submitted_tasks()
Wait for every activation registered with the shared task group.
Definition tbb_scheduler.hpp:275
virtual void dispatch_handle(TaskHandle handle)
Route one runnable task activation to its execution arena.
Definition tbb_scheduler.hpp:308
void schedule(AsyncTask &&t) override
Schedule a coroutine for initial execution.
Definition tbb_scheduler.hpp:96
Validated CUDA device identity with cached immutable capabilities.
Definition device.hpp:105
Temporarily select one CUDA device and restore the previous device on destruction.
Definition runtime.hpp:44
cuda::ResourceLease< HandleSlot > handle
Definition cublas/execution.cpp:33
Structured CUDA runtime failures and checked-call helpers.
Defines the CUDA task admission type and scheduler interface.
CUDA device discovery and immutable hardware capability snapshots.
Definition qnum.hpp:403
This represents the reverse-mode adjoint for functions of complex variables, assuming a real-valued s...
Definition async/async.hpp:26
CudaTaskPromise & cuda_promise(TaskHandle handle) noexcept
Narrow an erased task promise after verifying that it is CUDA-specific.
Definition async_task_promise.hpp:707
void check(cudaError_t status, std::string_view operation, std::optional< int > device, std::source_location where)
Raise when a CUDA runtime operation did not return cudaSuccess.
Definition cuda_error.cpp:44
CUDA device, stream, resource, and scoped runtime primitives.
Execution requirements used to validate a scheduler route.
Definition async_task.hpp:43
Arena configuration for the unified host/CUDA TBB scheduler.
Definition tbb_cuda_scheduler.hpp:113
int host_max_concurrency
Maximum participation in the host arena.
Definition tbb_cuda_scheduler.hpp:115
int cuda_max_concurrency_per_device
Maximum worker participation in each CUDA device arena.
Definition tbb_cuda_scheduler.hpp:119
std::optional< int > default_cuda_device
Device used to route CUDA tasks that have not established affinity.
Definition tbb_cuda_scheduler.hpp:122
Selects the task-registry implementation for the current build mode.
Scheduler implementation using oneAPI oneTBB task_arena + task_group.
Internal non-blocking oneTBB arena admission helper.
#define CHECK(cond,...)
Definition trace.hpp:299
#define PANIC(...)
Definition trace.hpp:382
#define CHECK_EQUAL(a, b,...)
Definition trace.hpp:309
#define DEBUG_CHECK_EQUAL(a, b,...)
Definition trace.hpp:618