|
Uni20
Asynchronous tensor and runtime library.
|
Schedulers answer one question: when a ready coroutine should run.
They do not define dependency legality; epochs do that. If a coroutine is not causally ready, it will still suspend regardless of scheduler choice.
A scheduler owns queued runnable activations, not every coroutine whose promise records that scheduler. Before admission, the public AsyncTask or CudaTask owns the frame. After a running coroutine suspends, its awaiter owns the BasicTask until readiness publishes another activation. The recorded scheduler pointer is only a non-owning resumption route. Consequently, a scheduler does not know about a task merely because that task names it, and scheduler quiescence does not include externally suspended frames.
Current tasks normally retain the scheduler installed when they are first scheduled. A nested AsyncTask with a different scheduler is submitted there, then returns its continuation to the continuation's recorded scheduler. A suspended, externally owned task may replace its route with any scheduler that accepts its domain and CUDA device requirements. A user-facing migration awaiter remains future work. See Scheduler Routing and Task Domains.
Most async code uses the global scheduler helpers from debug_scheduler.hpp:
set_global_scheduler(...)get_global_scheduler()reset_global_scheduler()schedule(AsyncTask&&)ScopedSchedulerIf you do not override it, the global scheduler defaults to an internal DebugScheduler.
ScopedScheduler is the standard way to override scheduler context in tests. It changes the selected scheduler but does not own it or extend its lifetime. Any scheduler supplied to it must remain alive until all tasks routed through that scheduler have completed or can no longer resume. See Lifetime and Quiescence for the full contract, including stack-local test schedulers.
IAsyncScheduler::execute_batch(count, callable) executes a synchronous batch of independent ordinary function calls. The free execute_batch(...) helper uses the active global scheduler. On successful return, every index in [0, count) has been invoked exactly once and the callable is no longer borrowed by the scheduler.
A lightweight batch is deliberately not a collection of AsyncTask coroutines:
DebugScheduler executes indices deterministically using its configured fifo, reverse, or seeded random ordering. TbbScheduler executes every batch inside its arena, using a serial loop for one participant and a oneTBB parallel_for otherwise. This includes serial batches in the same concurrency budget as enqueued coroutine activations. An optional TbbSchedulerBatchOptions::maximum_concurrency cap uses that many dynamic runners over the batch index space, independently of the arena's coroutine-task concurrency. TbbNumaScheduler selects one managed arena for the batch; placement-aware partitioning across NUMA nodes remains a later policy.
A batch item may call ordinary scheduler APIs. In particular, scheduling work and then using get_wait() is supported by both the recursive DebugScheduler progress path and TBB's resumable nested-wait path, including an arena with one participant. This is a composability guarantee, not the normal batch pattern: block-level batches should normally call immediate dense operations directly.
| Scheduler | Strength | Tradeoff | Typical use |
|---|---|---|---|
DebugScheduler | deterministic, simple deadlock diagnostics | single-threaded | semantics tests, debugging |
TbbScheduler | parallel throughput | non-deterministic task interleaving | parallel CPU workloads |
TbbNumaScheduler | NUMA-aware dispatch over per-node TBB arenas | extra dispatch complexity | NUMA-sensitive workloads |
DebugCudaScheduler | deterministic unified host/multi-device execution | calling-thread only | CUDA semantics tests and bring-up |
TbbCudaScheduler | parallel unified host/multi-device execution | non-deterministic task interleaving | CPU work plus asynchronous CUDA submission and provider calls |
Execution model:
run() snapshots and executes one runnable batchDebugSchedulerOptions::order selects fifo, reverse, or random batch order; the default is reverse, preserving the scheduler's historical behaviorDebugSchedulerOptions::random_seed, which defaults to zerorun_all() drains until queue emptyWait/deadlock behavior:
help_while_waiting(...) drives runnable tasks while waitingTaskRegistry::dump() and aborts with deadlock diagnosticUse DebugScheduler as the first tool for dependency bugs.
Execution model:
task_arena + task_groupsched_yield retention looptask_group::defer() and publish them through non-blocking task_arena::enqueue()task_arena::execute()run_all() resumes if paused, then waits for task-group completion of the currently submitted activationsparallel_for in the arena; an optional dynamic-runner cap limits simultaneous batch items independently of arena concurrency, and batches remain separate from coroutine activation accountingThe task group owns each scheduled resumption while it runs. If that resumption suspends the coroutine on an epoch or external event, the TBB task finishes and the awaiter owns the suspended coroutine until it becomes ready. run_all() is therefore scheduler-activation quiescence, not proof that every coroutine ever routed through the scheduler has completed.
An admission that happens-before run_all() is included because its deferred task is already registered with the group. If admission and run_all() are unordered and concurrent, either may linearize first; run_all() is not a global barrier against submissions that have not yet been accepted.
Pause/resume:
pause() queues handles without dispatchresume() drains queued handles and dispatches themWait behavior:
get_wait() enters the scheduler arena through task_arena::execute()There is no internal wait timeout or idle-work bookkeeping. An idle arena can still receive later work or external completion. Use an attached debugger or an instrumented TaskRegistry diagnostics service to investigate stalled waits; the latter can consume a signal such as SIGUSR1 and write a best-effort DAG snapshot. See `tbb_execution_primer.md` for the execution model, notification contract, and single-participant timeline.
TbbNumaScheduler manages multiple TbbScheduler arenas, one per NUMA node.
Dispatch policy:
Diagnostics:
scheduled_count_for(node) reports dispatch counts used by testsDebugCudaScheduler and TbbCudaScheduler implement typed CUDA admission for CudaTask. They share ordinary BasicTask rescheduling and continuation routing with the host schedulers.
DebugCudaScheduler also admits AsyncTask. It uses one deterministic queue for both domains and selects either the CUDA promise's affinity or the scheduler's default device around each CUDA activation. One instance can execute tasks for multiple devices and restores the calling thread between activations.TbbCudaScheduler extends TbbScheduler with one worker-only arena per enrolled CUDA device. The host and device arenas share task-group, pause, wait and rescheduling state.TbbCudaSchedulerOptions::cuda_max_concurrency_per_device explicitly retains the capability to measure or use concurrent submission. This limit is independent of stream count and provider-handle pool sizes.task_scheduler_observer. Every participating worker saves its previous CUDA device, selects the arena device, and restores the previous selection on exit.co_await cuda::set_device(device) establishes or changes affinity; it continues immediately when that device is already current and otherwise resubmits the task through the selected device arenarun_all() and get_wait() cover currently submitted host and CUDA activations through the shared task group. They do not complete a coroutine suspended on an external buffer, CUDA completion, or resource awaiter.Tasks running in a CUDA scheduler arena must not call cudaSetDevice directly. Cross-device work re-enters the scheduler through the target device arena; this keeps the observer authoritative for the whole arena activation.
Debug builds verify the thread's current CUDA device before each oneTBB CUDA activation. Leaf kernel wrappers do not inspect coroutine promises; they check their CUDA API results and may validate stream and buffer device compatibility.
An unbound nested task inherits its parent scheduler only within the same task domain. Crossing between host and CUDA requires explicit prior admission or route binding. CUDA task admission is not global yet. The process-wide CUDA runtime now provides canonical per-device resources, but installing or choosing an ordinary default scheduler remains a separate future policy.
DebugSchedulerTbbSchedulerTbbNumaScheduler when NUMA topology materially affects performanceDebugCudaScheduler to diagnose CUDA task ordering deterministicallyTbbCudaScheduler for parallel CUDA submission once device placement is explicitIf behavior differs between debug and TBB schedulers, suspect missing dependency edges, missing releases, or lifetime bugs before suspecting scheduler implementation.
runtime_model.mdtbb_execution_primer.mdbuffers_and_awaiters.mdtask_registry_debug.mdquick_reference.md