|
Uni20
Asynchronous tensor and runtime library.
|
Status: typed CUDA buffers and scoped read/write guards are implemented by uni20::cuda::CudaBuffer<T>, uni20::cuda::ReadAccess<T>, and uni20::cuda::WriteAccess<T>. The filename is retained for existing links; this document supersedes the earlier GpuEpochQueue proposal.
This note defines how an ordering already established by ordinary C++ control flow or Uni20's async runtime is lowered to CUDA stream synchronization. CUDA does not maintain a second epoch chain.
The implemented guards expose explicit release and enforce the live-access contract described below.
Related notes:
EpochQueue, readers, writers, and causal readiness.CUDA submission builds a forward-only partial order. A stream can wait for a completion that has already been recorded, but this layer cannot register a consumer before its producer exists. Out-of-order construction belongs to EpochQueue, not to CUDA events.
Treat CudaBuffer<T> like an ordinary mutable value whose storage happens to be on a GPU:
Async<CudaBuffer<T>> establishes them through its CPU ReadBuffer and WriteBuffer epochs.Releasing a CUDA access guard does not wait for the GPU operation to finish. It records a completion at the current stream tail and publishes that token to the buffer. A causally later operation can therefore be submitted immediately: its stream waits asynchronously for the published completion even when the GPU has a large backlog.
The caller owns access ordering:
ReadBuffer or WriteBuffer that made the operation runnable;EpochQueue therefore ensures that a writer is not submitted until every preceding reader has published its CUDA completion;This means the CUDA buffer state represents only unfinished operations from the already-ordered past. It needs no generation number, current epoch, runnable queue, task wakeup mechanism, or value ownership.
cuda::DeviceResources
: Owns one validated Device, an actually-idle StreamPool, and lazily constructed provider resources. The installed CUDA runtime normally owns one canonical instance per enrolled device. The resource set must outlive its buffers and streams.
cuda::CudaBuffer<T>
: A move-only owner of one typed CUDA allocation. Its raw device pointer is exposed only through scoped access guards. Its completion ledger consists of the latest exclusive-writer completion and reader completions submitted since that writer. Live guard counts validate ordinary read/write access rules; they do not order callers or make them wait. The buffer does not own an EpochQueue or task wakeup state. A per-buffer mutex protects short ledger snapshots and publication; independent buffers do not share it.
cuda::ReadAccess<T>
: A scoped read-only guard constructed by buffer.read_synchronized_with(stream). Construction installs the latest-writer wait on stream and exposes T const*. Destruction records and publishes a reader completion at the stream tail.
cuda::WriteAccess<T>
: A scoped read/write guard constructed by buffer.write_synchronized_with(stream). Construction waits on the latest writer and every unfinished reader, then exposes T*. Destruction records and publishes an exclusive-writer completion at the stream tail. In-place operations use one write guard; they do not add a separate read guard for the same buffer.
cuda::AccessCompletion
: Records one operation-tail completion after a backend has submitted all work using several access guards. Releasing every participating guard through the object publishes the same immutable completion to each buffer ledger. If the event cannot be recorded, construction synchronizes the stream and subsequent releases return their live tokens without publishing an event.
cuda::Stream
: A reference-counted lease of one StreamPool slot. Access guards keep a copy so they can record completions. The final stream reference schedules pool return after all queued work and access-completion events finish.
cuda::Completion
: An immutable shared token for one recorded non-timing CUDA event. Retaining a completion in buffer state does not share or reference-count the buffer itself.
The buffer state is equivalent to:
Each access guard owns one live host-side access token. Moving a guard transfers that token and leaves the source inert. Explicit release() or destruction returns the token after publishing the guard's device completion.
A read access:
live_write_guard is set;live_read_guards;writer_completion, when present;reader_completions, then decrements live_read_guards.An exclusive write access:
live_write_guard is set or live_read_guards != 0;live_write_guard;writer_completion and every unfinished reader completion;writer_completion and clears the prior reader completions;live_write_guard.These checks diagnose invalid host access; they do not introduce a reservation queue. A conflicting acquisition never waits for another guard to release and never suspends a coroutine. Correctly ordered code releases the predecessor guard first, making its recorded completion immediately available to the successor.
Completed readers are pruned opportunistically before their retained vector needs to grow and when a writer snapshots its dependencies. This bounds event retention for long-lived read-mostly inputs without introducing an epoch or generation counter.
A typical operation is queued as:
Lexical destruction remains the independent single-guard release path. A backend operation with several guards uses AccessCompletion so equivalent operation-tail dependencies share one event.
Each access guard performs these construction steps:
Independent explicit release or guard destruction records one completion at the stream tail. Operation-level release records one shared completion before briefly locking each buffer state to publish it and return the corresponding live access token. Publication and token return are one state transition: a causally later conflicting acquisition sees either the live predecessor guard or its recorded completion. The state mutex is not held while the caller launches CUDA work. Independent operations and compatible readers can therefore queue and execute concurrently.
The construction snapshot and release publication are intentionally not one transaction. Their correctness follows from the causal contract: a conflicting successor cannot enter acquisition until its predecessors have released, while compatible readers do not need to observe one another.
The blocking StreamPool::acquire() path is the bring-up path for the blocking CUDA submission channel. The non-blocking channel will suspend through a Uni20 scheduler while stream resources are unavailable, then use the same access construction, launch, and release-publication rules. CUDA buffer access itself never needs an awaiter: once the stream and any provider resources have been acquired, read_synchronized_with(stream) and write_synchronized_with(stream) perform only bounded host work. Async CUDA lowering must use the non-blocking channel for resources that can be unavailable; using blocking resource acquisition inside an async operation is a scheduler-policy error, not an optimization choice.
Scoped stream and buffer access are RAII resources.
AccessCompletion applies the same synchronized fallback before returning any participating live token.Stream reference enqueues the stream-pool return callback at the current stream tail; the slot becomes idle only when that callback runs.cudaFreeAsync for stream-ordered allocations, or cudaFree for the synchronous fallback.CudaBuffer uses the stream-ordered allocator when device capabilities allow it, but higher-level tensor storage still needs the allocator policy described in Memory Allocation, while preserving the same completion and lifetime contract.
The current layer does not provide:
operator[] for device memory;Those capabilities belong to later storage, scheduler, and backend checkpoints.