device/cuda.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | /// @file cuda.h | ||
| 3 | /// @brief Zero-copy interop with CUDA, and through it PyTorch, Warp and any DLPack consumer: | ||
| 4 | /// Ernie's shared memory mapped into CUDA in place, and CUDA streams ordered with Ernie's queues | ||
| 5 | /// through shared timelines, so neither the host nor a copy sits between them. The CUDA driver | ||
| 6 | /// is loaded at run time: Ernie builds without the CUDA toolkit and runs without a CUDA driver. | ||
| 7 | #pragma once | ||
| 8 | |||
| 9 | #include "ernie/device/column.h" | ||
| 10 | #include "ernie/util/library.h" | ||
| 11 | #include "ernie/util/tensor.h" | ||
| 12 | |||
| 13 | #include <cstdint> | ||
| 14 | #include <memory> | ||
| 15 | #include <mutex> | ||
| 16 | #include <optional> | ||
| 17 | #include <unordered_map> | ||
| 18 | |||
| 19 | namespace ernie { | ||
| 20 | |||
| 21 | /// @brief The CUDA driver API's handles, declared here so no CUDA header is needed. | ||
| 22 | using CUresult = int; | ||
| 23 | using CUdevice = int; | ||
| 24 | using CUdeviceptr = std::uint64_t; | ||
| 25 | using CUcontext = struct CUctx_st*; | ||
| 26 | using CUstream = struct CUstream_st*; | ||
| 27 | using CUexternalMemory = struct CUextMemory_st*; | ||
| 28 | using CUexternalSemaphore = struct CUextSemaphore_st*; | ||
| 29 | using CUmemGenericAllocationHandle = unsigned long long; | ||
| 30 | |||
| 31 | /// @brief The CUDA driver API functions Ernie uses, by their ABI names. | ||
| 32 | 6 | struct CudaDriver { | |
| 33 | SharedLibrary library; | ||
| 34 | CUresult (*init)(unsigned int) = nullptr; | ||
| 35 | CUresult (*deviceGetCount)(int*) = nullptr; | ||
| 36 | CUresult (*deviceGet)(CUdevice*, int) = nullptr; | ||
| 37 | CUresult (*deviceGetUuid)(unsigned char*, CUdevice) = nullptr; | ||
| 38 | CUresult (*primaryCtxRetain)(CUcontext*, CUdevice) = nullptr; | ||
| 39 | CUresult (*primaryCtxRelease)(CUdevice) = nullptr; | ||
| 40 | CUresult (*ctxPushCurrent)(CUcontext) = nullptr; | ||
| 41 | CUresult (*ctxPopCurrent)(CUcontext*) = nullptr; | ||
| 42 | CUresult (*ctxSynchronize)() = nullptr; | ||
| 43 | CUresult (*importExternalMemory)(CUexternalMemory*, const void*) = nullptr; | ||
| 44 | CUresult (*externalMemoryGetMappedBuffer)(CUdeviceptr*, CUexternalMemory, | ||
| 45 | const void*) = nullptr; | ||
| 46 | CUresult (*destroyExternalMemory)(CUexternalMemory) = nullptr; | ||
| 47 | CUresult (*importExternalSemaphore)(CUexternalSemaphore*, const void*) = nullptr; | ||
| 48 | CUresult (*signalExternalSemaphoresAsync)(const CUexternalSemaphore*, const void*, unsigned int, | ||
| 49 | CUstream) = nullptr; | ||
| 50 | CUresult (*waitExternalSemaphoresAsync)(const CUexternalSemaphore*, const void*, unsigned int, | ||
| 51 | CUstream) = nullptr; | ||
| 52 | CUresult (*destroyExternalSemaphore)(CUexternalSemaphore) = nullptr; | ||
| 53 | CUresult (*memFree)(CUdeviceptr) = nullptr; | ||
| 54 | CUresult (*memAlloc)(CUdeviceptr*, std::size_t) = nullptr; | ||
| 55 | CUresult (*memcpyDtoDAsync)(CUdeviceptr, CUdeviceptr, std::size_t, CUstream) = nullptr; | ||
| 56 | CUresult (*memcpyDtoH)(void*, CUdeviceptr, std::size_t) = nullptr; | ||
| 57 | CUresult (*memsetD32Async)(CUdeviceptr, unsigned int, std::size_t, CUstream) = nullptr; | ||
| 58 | CUresult (*streamSynchronize)(CUstream) = nullptr; | ||
| 59 | CUresult (*streamCreate)(CUstream*, unsigned int) = nullptr; | ||
| 60 | CUresult (*streamDestroy)(CUstream) = nullptr; | ||
| 61 | // Virtual memory management, whose allocations can be exported. | ||
| 62 | CUresult (*memCreate)(CUmemGenericAllocationHandle*, std::size_t, const void*, | ||
| 63 | unsigned long long) = nullptr; | ||
| 64 | CUresult (*memRelease)(CUmemGenericAllocationHandle) = nullptr; | ||
| 65 | CUresult (*memAddressReserve)(CUdeviceptr*, std::size_t, std::size_t, CUdeviceptr, | ||
| 66 | unsigned long long) = nullptr; | ||
| 67 | CUresult (*memAddressFree)(CUdeviceptr, std::size_t) = nullptr; | ||
| 68 | CUresult (*memMap)(CUdeviceptr, std::size_t, std::size_t, CUmemGenericAllocationHandle, | ||
| 69 | unsigned long long) = nullptr; | ||
| 70 | CUresult (*memUnmap)(CUdeviceptr, std::size_t) = nullptr; | ||
| 71 | CUresult (*memSetAccess)(CUdeviceptr, std::size_t, const void*, std::size_t) = nullptr; | ||
| 72 | CUresult (*memGetAllocationGranularity)(std::size_t*, const void*, int) = nullptr; | ||
| 73 | CUresult (*memRetainAllocationHandle)(CUmemGenericAllocationHandle*, void*) = nullptr; | ||
| 74 | CUresult (*memGetAllocationPropertiesFromHandle)(void*, CUmemGenericAllocationHandle) = nullptr; | ||
| 75 | CUresult (*memExportToShareableHandle)(void*, CUmemGenericAllocationHandle, int, | ||
| 76 | unsigned long long) = nullptr; | ||
| 77 | CUresult (*memGetAddressRange)(CUdeviceptr*, std::size_t*, CUdeviceptr) = nullptr; | ||
| 78 | CUresult (*getErrorString)(CUresult, const char**) = nullptr; | ||
| 79 | |||
| 80 | /// @brief Exportable device memory, as `AllocateShareable` makes it. | ||
| 81 | struct Shareable { | ||
| 82 | CUdeviceptr pointer = 0; | ||
| 83 | CUmemGenericAllocationHandle handle = 0; | ||
| 84 | std::size_t size = 0; | ||
| 85 | }; | ||
| 86 | |||
| 87 | /// @return The driver, loaded and initialized once per process; null when there is none. | ||
| 88 | static const CudaDriver* Get(); | ||
| 89 | /// @brief Allocates at least `bytes` on device `ordinal` through virtual memory management, | ||
| 90 | /// with handles another API imports in place. Needs a current context. | ||
| 91 | /// @return The allocation, or none on failure. | ||
| 92 | std::optional<Shareable> AllocateShareable(int ordinal, std::size_t bytes) const; | ||
| 93 | void FreeShareable(const Shareable& allocation) const; | ||
| 94 | /// @throws DeviceError when `result` is an error. | ||
| 95 | void Check(CUresult result, std::string_view what) const; | ||
| 96 | }; | ||
| 97 | |||
| 98 | /// @brief Exportable allocations by address, as `CudaDriver::AllocateShareable` makes them, for | ||
| 99 | /// whoever hands out addresses and takes them back: Ernie's interop, and PyTorch's allocator hook. | ||
| 100 | ✗ | class ShareableAllocations { | |
| 101 | public: | ||
| 102 | ✗ | explicit ShareableAllocations(const CudaDriver& driver) : driver_(driver) {} | |
| 103 | ShareableAllocations(const ShareableAllocations&) = delete; | ||
| 104 | ShareableAllocations& operator=(const ShareableAllocations&) = delete; | ||
| 105 | |||
| 106 | /// @return The allocation's address, or 0 on failure. Needs a current context. | ||
| 107 | CUdeviceptr Allocate(int ordinal, std::size_t bytes); | ||
| 108 | /// @brief Frees what `Allocate` returned; other addresses are left alone. | ||
| 109 | void Free(CUdeviceptr pointer); | ||
| 110 | /// @brief Frees every allocation left. | ||
| 111 | void FreeAll(); | ||
| 112 | |||
| 113 | private: | ||
| 114 | const CudaDriver& driver_; | ||
| 115 | std::mutex mutex_; | ||
| 116 | std::unordered_map<CUdeviceptr, CudaDriver::Shareable> live_; | ||
| 117 | }; | ||
| 118 | |||
| 119 | /// @brief The CUDA device paired with a context's device by UUID. Columns in | ||
| 120 | /// `MemoryUse::Shared` arenas are viewed as CUDA tensors in place; each arena is mapped once. | ||
| 121 | /// Streams are CUDA streams (`torch.cuda.current_stream().cuda_stream`, `wp.get_stream()`), 0 | ||
| 122 | /// for the default one. | ||
| 123 | class CudaInterop { | ||
| 124 | public: | ||
| 125 | /// @return The interop, or null when there is no CUDA driver, no CUDA device with the | ||
| 126 | /// context's UUID, or the device shares no memory or timelines. | ||
| 127 | static std::unique_ptr<CudaInterop> Create(DeviceContext& context); | ||
| 128 | ~CudaInterop(); | ||
| 129 | CudaInterop(const CudaInterop&) = delete; | ||
| 130 | CudaInterop& operator=(const CudaInterop&) = delete; | ||
| 131 | |||
| 132 | /// @return The CUDA device ordinal, as `cuda:N` names it. | ||
| 133 | ✗ | int Ordinal() const { | |
| 134 | ✗ | return ordinal_; | |
| 135 | } | ||
| 136 | ✗ | const CudaDriver& Driver() const { | |
| 137 | ✗ | return driver_; | |
| 138 | } | ||
| 139 | |||
| 140 | /// @brief A CUDA tensor over the column's rows, in place, shaped (rows, element...). It is | ||
| 141 | /// valid while the column keeps its range; order access with `Wait` and `Signal`. | ||
| 142 | /// @throws DeviceError unless the column's arenas are `MemoryUse::Shared`. | ||
| 143 | Tensor View(const DeviceColumn& column); | ||
| 144 | /// @brief Makes work enqueued later on `stream` wait for `ticket`, without the host. | ||
| 145 | void Wait(CUstream stream, const Ticket& ticket); | ||
| 146 | /// @return A ticket reached once the work enqueued on `stream` so far is done, for Ernie's | ||
| 147 | /// queues to wait on. | ||
| 148 | Ticket Signal(CUstream stream); | ||
| 149 | /// @brief Makes `tensor`'s values the column's rows, after the work on `stream` so far: in | ||
| 150 | /// place, with no copy, when `tensor` is the column's own view; otherwise by one copy on the | ||
| 151 | /// device, counted in `DeviceContext::Transfers`. | ||
| 152 | /// @return The ticket Ernie's queues wait on before reading the rows. | ||
| 153 | /// @throws std::invalid_argument unless `tensor` is a contiguous CUDA tensor on this device | ||
| 154 | /// with the column's byte size. | ||
| 155 | Ticket Assign(const DeviceColumn& column, const Tensor& tensor, CUstream stream); | ||
| 156 | /// @brief A device column over a CUDA tensor's own memory, in place, when that memory is | ||
| 157 | /// exportable: allocated through CUDA's virtual memory management with shareable handles, as | ||
| 158 | /// `Allocate` and PyTorch's expandable segments allocate. Ordinary `cudaMalloc` memory is | ||
| 159 | /// not; assign such tensors to an Ernie column instead. | ||
| 160 | /// @return The column, shaped (rows, element...) as the tensor is and holding it alive; null | ||
| 161 | /// when its memory cannot be shared. | ||
| 162 | std::unique_ptr<DeviceColumn> Wrap(const Tensor& tensor, std::string_view name); | ||
| 163 | /// @brief Allocates `bytes` of exportable device memory, which `Wrap` shares in place. | ||
| 164 | /// @return Its address, or 0 on failure. | ||
| 165 | CUdeviceptr Allocate(std::size_t bytes); | ||
| 166 | /// @brief Frees what `Allocate` returned. | ||
| 167 | void Free(CUdeviceptr pointer); | ||
| 168 | |||
| 169 | /// @brief Makes the device's primary context current on this thread for a scope, for calls | ||
| 170 | /// to the driver directly. | ||
| 171 | class Current { | ||
| 172 | public: | ||
| 173 | explicit Current(const CudaInterop& interop); | ||
| 174 | ~Current(); | ||
| 175 | Current(const Current&) = delete; | ||
| 176 | Current& operator=(const Current&) = delete; | ||
| 177 | |||
| 178 | private: | ||
| 179 | const CudaDriver& driver_; | ||
| 180 | }; | ||
| 181 | |||
| 182 | private: | ||
| 183 | struct Mapping { | ||
| 184 | CUexternalMemory memory = nullptr; | ||
| 185 | CUdeviceptr pointer = 0; | ||
| 186 | }; | ||
| 187 | |||
| 188 | /// A stream's own timeline, which only that stream signals, so its values only increase. | ||
| 189 | ✗ | struct StreamTimeline { | |
| 190 | std::unique_ptr<Timeline> timeline; | ||
| 191 | CUexternalSemaphore semaphore = nullptr; | ||
| 192 | std::uint64_t signalled = 0; | ||
| 193 | }; | ||
| 194 | |||
| 195 | CudaInterop(DeviceContext& context, const CudaDriver& driver, CUdevice device, int ordinal); | ||
| 196 | /// @throws std::invalid_argument unless `tensor` is a contiguous tensor on this device. | ||
| 197 | void CheckOnDevice(const Tensor& tensor, std::string_view what) const; | ||
| 198 | CUdeviceptr Map(const Buffer& buffer); | ||
| 199 | /// Imports one of the context's timelines into CUDA, once. Takes `mutex_`'s lock as held. | ||
| 200 | CUexternalSemaphore Import(const Timeline& timeline); | ||
| 201 | |||
| 202 | DeviceContext& context_; | ||
| 203 | const CudaDriver& driver_; | ||
| 204 | CUdevice device_; | ||
| 205 | int ordinal_; | ||
| 206 | CUcontext cuda_ = nullptr; | ||
| 207 | std::mutex mutex_; | ||
| 208 | /// Mapped buffers by `Buffer::Id`, which no later buffer reuses. | ||
| 209 | std::unordered_map<std::uint64_t, Mapping> mappings_; | ||
| 210 | std::unordered_map<const Timeline*, CUexternalSemaphore> semaphores_; | ||
| 211 | std::unordered_map<CUstream, StreamTimeline> streams_; | ||
| 212 | ShareableAllocations allocations_; | ||
| 213 | }; | ||
| 214 | |||
| 215 | } // namespace ernie | ||
| 216 |