|
Uni20
Asynchronous tensor and runtime library.
|
Status: the first native Tensor backend is implemented. It provides a blocking real float/double exact SVD for tall column-major CUDA matrices and a device-resident BlockTensor sector bridge used by the first CUDA two-site DMRG sweep path.
The generic host-execution and dispatch contract is defined in CUDA Kernel Dispatch and Device Scheduling. This note keeps the cuSOLVER-specific consequences.
The key interpretation for uni20 is:
cudaMemcpy calls are blocking; asynchronous copies still become blocking if the API boundary immediately waits for the stream.Uni20 has one canonical DeviceResources set per enrolled CUDA device. It owns the actually-idle stream pool and lazily constructs a bounded cuSOLVER handle pool:
The handle pool belongs to the device rather than to scheduler threads. Host submission concurrency, stream capacity, and cuSOLVER handle capacity are separate tunables. The scheduler must retain the capability to use multiple host submitters for a device, but one is the default for general fine-grained CUDA work. The initial target for cuSOLVER is two handles per device, independent of that default submitter count. The canonical pool now implements that default, capped by stream capacity. cusolver::execution_pool(resources, count) may set a different count before the first provider use.
A solver request acquires an exclusive cuSOLVER handle first and then an actually-idle stream. The first blocking backend allocates workspace with cudaMallocAsync, binds the handle to the leased stream, calls the provider, copies devInfo to host, and synchronizes that stream before returning. Keeping handles independent from streams lets a handle rotate onto whichever pool stream becomes idle next.
Do not create and destroy cuSOLVER handles per operation. Handle creation is runtime state setup, not useful numerical work. Handles are device-local leased resources, not permanent properties of particular oneTBB worker threads.
An exclusive handle lease may move between physical workers in the same device arena because the arena observer establishes the same CUDA device on every participant. Different operations must not mutate one handle concurrently. Provider-specific state that remains live after host-call return stays retained until the relevant completion boundary.
The operation may rotate through the bounded stream pool. Workspace and completion tracking follow the submitted operation and selected stream, not the handle alone.
Multiple handle leases allow independent solver work with host-side concurrency. A cuSOLVER call such as SVD may occupy one device-scheduler participant while launching and coordinating many device kernels, even though device work may remain pending after the API returns. Scheduler concurrency and handle-pool capacity therefore provide separate bounds.
Multiple cuSOLVER leases are already useful at the blocking boundary. A scheduler batch can run independent symmetry sectors from multiple application or oneTBB worker threads; each participant blocks only on its own provider call and operation stream. A future asynchronous backend can retain the same pool while publishing GPU completion tokens instead of synchronizing in the call.
Raw cuSOLVER handles remain internal to RAII leases and backend adapters. An internal coroutine may await ordered resource admission, but the actual backend walk and cuSOLVER call are ordinary non-coroutine code. No further co_await occurs while the provider call is in progress.
The first implementation runs cuSOLVER directly from the active scheduler batch participant. A separate provider scheduler or lane is justified only if profiling shows that host-intensive calls starve lightweight submission or unrelated device continuations.
The main numerical cuSOLVER calls should be treated as potentially asynchronous device-work submissions. They can still perform nontrivial CPU-side setup, and the amount of host work is implementation- and routine-dependent, so profiling with Nsight Systems remains the authority for performance decisions.
Known host-side boundaries are:
cusolverDnSetStream, which mutates the handle state for the next call;cudaMemcpy between host and device;cudaStreamSynchronize, cudaDeviceSynchronize, or cudaEventSynchronize;devInfo, singular values, or factor data back to host and then waiting for those copies.For example, cusolverDnDgesvd writes devInfo on the device. A wrapper that needs devInfo on the CPU before returning must enqueue a D2H copy and wait, so the wrapper is host-blocking even if the solver call itself was submitted asynchronously.
Calling a cuSOLVER routine and completing the solver operation are different boundaries. NVIDIA states that cuSOLVER keeps execution asynchronous as much as possible, but it does not promise that every provider call is a negligible or strictly nonblocking host operation. A call can perform substantial host-side planning, launch many kernels, or contain implementation-specific synchronization before it returns. The selected stream remains the authoritative completion boundary for the device outputs.
This permits one host submission lane to maintain multiple in-flight solver operations when the provider calls return before their device work completes:
The provider calls are serial on that host lane, but work already submitted to different streams may execute concurrently on the GPU. Each in-flight operation needs an exclusive handle because binding a stream mutates handle state. It also needs retained workspace, input/output access state, device devInfo, and any host destination used for an asynchronous result copy.
The current exact SVD wrapper does not use this model. It enqueues the factorization, copies devInfo asynchronously into a stack variable, and immediately synchronizes the stream. The stack destination and immediate error check therefore make the whole wrapper host-blocking. An asynchronous wrapper would instead retain a pinned host devInfo destination in the pending operation, record or publish the stream completion, and check the value only after completion. Singular values needed for global truncation can use the same submit-all-then-join structure.
One host lane is not an unconditional design requirement. If profiling shows that a particular solver routine spends enough time inside the host API call to underfeed the GPU, a small provider-specific submission lane may be useful. That limit must remain separate from general CUDA block-operation submission: allowing every fine-grained GEMM, copy, event, and callback operation to use the same host parallelism creates driver contention without increasing stream capacity.
Removing the explicit stream synchronization is simple at the provider call, but the retained operation must replace several assumptions currently supplied by that synchronization:
devInfo cannot be copied into a stack variable. A pending SVD must retain a pinned host destination, or retain device status storage, until its completion is collected.release_after_synchronization(). The existing CUDA buffer completion machinery already supports this.CudaBuffer transfers those completions to its shared allocation and queues cudaFreeAsync on the device-local reclamation stream, so ordinary tensor destruction does not restore host blocking.devInfo is deferred and must be checked when the pending result is collected.The current block-SVD path has an additional ordering constraint. Each sector function immediately copies its singular values to host after the SVD. That copy would wait for the same sector and preserve serialization even if the provider wrapper returned early. The useful one-submitter structure is therefore:
With a two-handle pool, the third submission may wait for either earlier handle to return. That is intentional admission control: one host lane can keep at most two solver operations in flight without creating unbounded workspace pressure. If any collection reports failure, all already-submitted sector operations must still be drained before the batch propagates the error. Performance measurement must also attribute completion rather than the return of the submission call; the current lightweight-batch item timer would otherwise report only enqueue time as sector SVD time.
The exact Tensor backend currently accepts four mutable CUDA-buffer mdspecs: the destructive input matrix, singular values, left vectors, and right-adjoint vectors. It supports real float and double, reduced or full vectors, tall column-major matrices, valid nonzero buffer offsets, and one CUDA device across all operands. Unsupported shapes or layouts decline before resource admission. Complex scalars, wide matrices, row-major lowering, singular-values-only forms, and an asynchronous Tensor entry point remain future work.
The BlockTensor bridge keeps bulk factorization data resident:
Each packed CUDA block retains an independent completion ledger even though all blocks share one physical allocation. This permits concurrent block transfers and multiple blocking cuSOLVER calls from different scheduler participants. Measured and unmeasured BlockTensor entry points share this same bridge; the measured form attributes the per-sector scheduler batch to its caller-supplied event. Repartitioned centers obtain device placement from their retained block descriptors rather than requiring access to an owning storage object. The current bridge requires one dense axis on each side of the SVD boundary and one CUDA device across all blocks. A full-vector sector whose charge occurs on only one boundary has no provider factorization to perform; the bridge constructs its side-specific identity null-space factor directly on CUDA.
The CUDA/cuSOLVER layer should not become a second dependency DAG. CPU-side epochs establish causal readiness: a GPU operation is submitted only once its metadata and object lifetime are valid. The current GPU runtime lowers this through scoped buffer.read_synchronized_with(stream) / buffer.write_synchronized_with(stream) guards and retained writer/reader completions, without reconstructing an independent epoch graph.
This keeps the split clear:
CudaTask activations in the arena for their effective device, selected from explicit affinity or the scheduler default, with bounded host participation;The current exact SVD entry point is ordinary blocking code rather than a CudaTask. It acquires resources inside the selected backend, executes without operation-tag redispatch, and returns after stream synchronization. Future coroutine entry points may await resource admission, but the provider call itself remains an ordinary non-coroutine leaf.
Temporary cuSOLVER workspace follows the device allocation capability. Devices with stream-ordered memory-pool support use cudaMallocAsync and cudaFreeAsync on the leased execution stream. Other devices use the blocking cudaMalloc and cudaFree fallback; the exact SVD boundary already synchronizes the execution stream before those allocations are released.