| // This file is part of Eigen, a lightweight C++ template library | |
| // for linear algebra. | |
| // | |
| // Copyright (C) 2014 Benoit Steiner <benoit.steiner.goog@gmail.com> | |
| // | |
| // This Source Code Form is subject to the terms of the Mozilla | |
| // Public License v. 2.0. If a copy of the MPL was not distributed | |
| // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| #if defined(EIGEN_USE_GPU) && !defined(EIGEN_TENSOR_TENSOR_DEVICE_GPU_H) | |
| #define EIGEN_TENSOR_TENSOR_DEVICE_GPU_H | |
| // IWYU pragma: private | |
| #include "./InternalHeaderCheck.h" | |
| #include "../../../../Eigen/src/Core/util/GpuHipCudaDefines.inc" | |
| #include "../../../../Eigen/src/Core/util/GpuRuntime.h" | |
| namespace Eigen { | |
| static const int kGpuScratchSize = 1024; | |
| // The device facts Eigen consults, read one at a time. The reason is portability, not speed: the opt-in | |
| // shared-memory limit and memory-pool support are not fields a gpuDeviceProp_t carries under both backends, while | |
| // the attribute enumerators exist in each. (Measured on CUDA 13.3, a warm gpuGetDeviceProperties costs 0.3-0.5 us | |
| // against 1.4-1.5 us for these nine queries, and a cold call of either is dominated by runtime initialization.) | |
| struct GpuDeviceAttributes { | |
| int multiProcessorCount; | |
| int maxThreadsPerBlock; | |
| int maxThreadsPerMultiProcessor; | |
| int sharedMemPerBlock; | |
| int sharedMemPerBlockOptin; | |
| int computeCapabilityMajor; | |
| int computeCapabilityMinor; | |
| int warpSize; | |
| int memoryPoolsSupported; | |
| }; | |
| // Filled once for every visible device at first use, which is thread-safe by the initialization rules and needs no | |
| // lock. A few microseconds per device, so there is nothing to gain from filling it per device on demand. | |
| // Templated on the attribute enum: CUDA spells it cudaDeviceAttr and HIP hipDeviceAttribute_t, and the .inc pair | |
| // aliases the enumerators but not the type. | |
| template <typename GpuDeviceAttr> | |
| inline int GetGpuDeviceAttribute(GpuDeviceAttr attribute, int device) { | |
| #if !defined(EIGEN_USE_HIP) | |
| if (attribute == gpuDevAttrMemoryPoolsSupported) { | |
| int driver_version = 0; | |
| EIGEN_GPU_RUNTIME_CHECK(cudaDriverGetVersion(&driver_version)); | |
| // CUDA minor-version compatibility permits drivers that predate this attribute. | |
| if (driver_version < 11020) return 0; | |
| } | |
| #endif | |
| int value = 0; | |
| const gpuError_t error = gpuDeviceGetAttribute(&value, attribute, device); | |
| #if defined(EIGEN_USE_HIP) | |
| // HIP 5.x declares the opt-in enumerator but does not implement the query. | |
| if (attribute == gpuDevAttrMaxSharedMemoryPerBlockOptin && error == hipErrorInvalidValue) { | |
| (void)hipGetLastError(); | |
| EIGEN_GPU_RUNTIME_CHECK(gpuDeviceGetAttribute(&value, gpuDevAttrMaxSharedMemoryPerBlock, device)); | |
| return value; | |
| } | |
| #endif | |
| EIGEN_GPU_RUNTIME_CHECK(error); | |
| return value; | |
| } | |
| inline const std::vector<GpuDeviceAttributes>& GetGpuDeviceAttributes() { | |
| // The device count is dynamic; keep the cache alive for GpuDevice users in static destructors. | |
| static const std::vector<GpuDeviceAttributes>* kAttributes = [] { | |
| int num_devices = 0; | |
| EIGEN_GPU_RUNTIME_CHECK(gpuGetDeviceCount(&num_devices)); | |
| auto* attributes = new std::vector<GpuDeviceAttributes>(num_devices); | |
| for (int device = 0; device < num_devices; ++device) { | |
| (*attributes)[device] = { | |
| /*multiProcessorCount=*/GetGpuDeviceAttribute(gpuDevAttrMultiProcessorCount, device), | |
| /*maxThreadsPerBlock=*/GetGpuDeviceAttribute(gpuDevAttrMaxThreadsPerBlock, device), | |
| /*maxThreadsPerMultiProcessor=*/GetGpuDeviceAttribute(gpuDevAttrMaxThreadsPerMultiProcessor, device), | |
| /*sharedMemPerBlock=*/GetGpuDeviceAttribute(gpuDevAttrMaxSharedMemoryPerBlock, device), | |
| /*sharedMemPerBlockOptin=*/GetGpuDeviceAttribute(gpuDevAttrMaxSharedMemoryPerBlockOptin, device), | |
| /*computeCapabilityMajor=*/GetGpuDeviceAttribute(gpuDevAttrComputeCapabilityMajor, device), | |
| /*computeCapabilityMinor=*/GetGpuDeviceAttribute(gpuDevAttrComputeCapabilityMinor, device), | |
| /*warpSize=*/GetGpuDeviceAttribute(gpuDevAttrWarpSize, device), | |
| /*memoryPoolsSupported=*/GetGpuDeviceAttribute(gpuDevAttrMemoryPoolsSupported, device), | |
| }; | |
| } | |
| return attributes; | |
| }(); | |
| return *kAttributes; | |
| } | |
| inline const GpuDeviceAttributes& GetGpuDeviceAttributes(int device) { | |
| const std::vector<GpuDeviceAttributes>& attributes = GetGpuDeviceAttributes(); | |
| eigen_assert(device >= 0 && device < static_cast<int>(attributes.size()) && "no such GPU device"); | |
| return attributes[device]; | |
| } | |
| // Attributes for StreamInterface implementations that follow the calling thread's current device. | |
| // GpuStreamDevice instead queries the device that owns its stream. | |
| inline const GpuDeviceAttributes& GetCurrentGpuDeviceAttributes() { | |
| int device = 0; | |
| EIGEN_GPU_RUNTIME_CHECK(gpuGetDevice(&device)); | |
| return GetGpuDeviceAttributes(device); | |
| } | |
| // This defines an interface that GPUDevice can take to use | |
| // HIP / CUDA streams underneath. | |
| class StreamInterface { | |
| public: | |
| virtual ~StreamInterface() = default; | |
| virtual const gpuStream_t& stream() const = 0; | |
| virtual const gpuDeviceProp_t& deviceProperties() const = 0; | |
| // The attributes of the device this interface's stream runs on. The default reports the device the calling | |
| // thread is bound to, which is what an implementation that follows the current device wants; one that owns a | |
| // device index overrides it, as GpuStreamDevice does, so that the answer follows the stream rather than | |
| // whatever device the caller happens to have selected. | |
| virtual const GpuDeviceAttributes& deviceAttributes() const { return GetCurrentGpuDeviceAttributes(); } | |
| // Allocate memory on the actual device where the computation will run | |
| virtual void* allocate(size_t num_bytes) const = 0; | |
| virtual void deallocate(void* buffer) const = 0; | |
| // Return a scratchpad buffer of size 1k | |
| virtual void* scratchpad() const = 0; | |
| // Return a semaphore. The semaphore is initially initialized to 0, and | |
| // each kernel using it is responsible for resetting to 0 upon completion | |
| // to maintain the invariant that the semaphore is always equal to 0 upon | |
| // each kernel start. | |
| virtual unsigned int* semaphore() const = 0; | |
| }; | |
| class GpuDeviceProperties { | |
| public: | |
| static const GpuDeviceProperties& instance() { | |
| static const GpuDeviceProperties* kInstance = new GpuDeviceProperties(); | |
| return *kInstance; | |
| } | |
| EIGEN_STRONG_INLINE const gpuDeviceProp_t& get(int device) const { | |
| eigen_assert(device >= 0 && device < static_cast<int>(device_properties_.size()) && "no such GPU device"); | |
| return device_properties_[device]; | |
| } | |
| private: | |
| GpuDeviceProperties() = default; | |
| static std::vector<gpuDeviceProp_t> GetDeviceProperties() { | |
| int num_devices = 0; | |
| EIGEN_GPU_RUNTIME_CHECK(gpuGetDeviceCount(&num_devices)); | |
| std::vector<gpuDeviceProp_t> device_properties(num_devices); | |
| for (int i = 0; i < num_devices; ++i) { | |
| EIGEN_GPU_RUNTIME_CHECK(gpuGetDeviceProperties(&device_properties[i], i)); | |
| } | |
| return device_properties; | |
| } | |
| std::vector<gpuDeviceProp_t> device_properties_ = GetDeviceProperties(); | |
| }; | |
| EIGEN_ALWAYS_INLINE const GpuDeviceProperties& GetGpuDeviceProperties() { return GpuDeviceProperties::instance(); } | |
| EIGEN_ALWAYS_INLINE const gpuDeviceProp_t& GetGpuDeviceProperties(int device) { | |
| return GetGpuDeviceProperties().get(device); | |
| } | |
| static const gpuStream_t default_stream = gpuStreamDefault; | |
| class GpuStreamDevice : public StreamInterface { | |
| public: | |
| // Use the default stream on the current device | |
| GpuStreamDevice() : stream_(&default_stream), scratch_(nullptr), semaphore_(nullptr) { | |
| EIGEN_GPU_RUNTIME_CHECK(gpuGetDevice(&device_)); | |
| } | |
| // Use the default stream on the specified device | |
| GpuStreamDevice(int device) : stream_(&default_stream), device_(device), scratch_(nullptr), semaphore_(nullptr) {} | |
| // Use the specified stream. Note that it's the | |
| // caller's responsibility to ensure that the stream can run on | |
| // the specified device. If no device is specified the code | |
| // assumes that the stream is associated to the current gpu device. | |
| GpuStreamDevice(const gpuStream_t* stream, int device = -1) | |
| : stream_(stream), device_(device), scratch_(nullptr), semaphore_(nullptr) { | |
| if (device < 0) { | |
| EIGEN_GPU_RUNTIME_CHECK(gpuGetDevice(&device_)); | |
| } else { | |
| int num_devices = 0; | |
| EIGEN_GPU_RUNTIME_CHECK(gpuGetDeviceCount(&num_devices)); | |
| EIGEN_UNUSED_VARIABLE(num_devices); | |
| gpu_assert(device < num_devices); | |
| device_ = device; | |
| } | |
| } | |
| ~GpuStreamDevice() override { | |
| if (scratch_) { | |
| deallocate(scratch_); | |
| } | |
| } | |
| const gpuStream_t& stream() const override { return *stream_; } | |
| const gpuDeviceProp_t& deviceProperties() const override { return GetGpuDeviceProperties(device_); } | |
| const GpuDeviceAttributes& deviceAttributes() const override { return GetGpuDeviceAttributes(device_); } | |
| void* allocate(size_t num_bytes) const override { | |
| EIGEN_GPU_RUNTIME_CHECK(gpuSetDevice(device_)); | |
| void* result = nullptr; | |
| EIGEN_GPU_RUNTIME_CHECK(gpuMalloc(&result, num_bytes)); | |
| // A zero-byte request (an empty tensor's temporary) succeeds with a null pointer. | |
| gpu_assert(result != nullptr || num_bytes == 0); | |
| return result; | |
| } | |
| void deallocate(void* buffer) const override { | |
| // The null pointer of a zero-byte allocate(); callers such as TensorForcedEval free it unconditionally. | |
| if (buffer == nullptr) return; | |
| EIGEN_GPU_RUNTIME_CHECK(gpuSetDevice(device_)); | |
| EIGEN_GPU_RUNTIME_CHECK(gpuFree(buffer)); | |
| } | |
| void* scratchpad() const override { | |
| if (scratch_ == nullptr) { | |
| scratch_ = allocate(kGpuScratchSize + sizeof(unsigned int)); | |
| } | |
| return scratch_; | |
| } | |
| unsigned int* semaphore() const override { | |
| if (semaphore_ == nullptr) { | |
| char* scratch = static_cast<char*>(scratchpad()) + kGpuScratchSize; | |
| semaphore_ = reinterpret_cast<unsigned int*>(scratch); | |
| EIGEN_GPU_RUNTIME_CHECK(gpuMemsetAsync(semaphore_, 0, sizeof(unsigned int), *stream_)); | |
| } | |
| return semaphore_; | |
| } | |
| private: | |
| const gpuStream_t* stream_; | |
| int device_; | |
| mutable void* scratch_; | |
| mutable unsigned int* semaphore_; | |
| }; | |
| struct GpuDevice { | |
| // The StreamInterface is not owned: the caller is | |
| // responsible for its initialization and eventual destruction. | |
| explicit GpuDevice(const StreamInterface* stream) : stream_(stream), max_blocks_(INT_MAX) { eigen_assert(stream); } | |
| // Nothing reads max_blocks_ any more; the executor sizes its grid from the device's own limits. | |
| EIGEN_DEPRECATED explicit GpuDevice(const StreamInterface* stream, int num_blocks) | |
| : stream_(stream), max_blocks_(num_blocks) { | |
| eigen_assert(stream); | |
| } | |
| // TODO(bsteiner): This is an internal API, we should not expose it. | |
| EIGEN_STRONG_INLINE const gpuStream_t& stream() const { return stream_->stream(); } | |
| EIGEN_STRONG_INLINE void* allocate(size_t num_bytes) const { return stream_->allocate(num_bytes); } | |
| EIGEN_STRONG_INLINE void deallocate(void* buffer) const { stream_->deallocate(buffer); } | |
| EIGEN_STRONG_INLINE void* allocate_temp(size_t num_bytes) const { return stream_->allocate(num_bytes); } | |
| EIGEN_STRONG_INLINE void deallocate_temp(void* buffer) const { stream_->deallocate(buffer); } | |
| template <typename Type> | |
| EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Type get(Type data) const { | |
| return data; | |
| } | |
| EIGEN_STRONG_INLINE void* scratchpad() const { return stream_->scratchpad(); } | |
| EIGEN_STRONG_INLINE unsigned int* semaphore() const { return stream_->semaphore(); } | |
| EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void memcpy(void* dst, const void* src, size_t n) const { | |
| #ifndef EIGEN_GPU_COMPILE_PHASE | |
| EIGEN_GPU_RUNTIME_CHECK(gpuMemcpyAsync(dst, src, n, gpuMemcpyDeviceToDevice, stream_->stream())); | |
| #else | |
| EIGEN_UNUSED_VARIABLE(dst); | |
| EIGEN_UNUSED_VARIABLE(src); | |
| EIGEN_UNUSED_VARIABLE(n); | |
| eigen_assert(false && "The default device should be used instead to generate kernel code"); | |
| #endif | |
| } | |
| EIGEN_STRONG_INLINE void memcpyHostToDevice(void* dst, const void* src, size_t n) const { | |
| EIGEN_GPU_RUNTIME_CHECK(gpuMemcpyAsync(dst, src, n, gpuMemcpyHostToDevice, stream_->stream())); | |
| } | |
| EIGEN_STRONG_INLINE void memcpyDeviceToHost(void* dst, const void* src, size_t n) const { | |
| EIGEN_GPU_RUNTIME_CHECK(gpuMemcpyAsync(dst, src, n, gpuMemcpyDeviceToHost, stream_->stream())); | |
| } | |
| EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void memset(void* buffer, int c, size_t n) const { | |
| #ifndef EIGEN_GPU_COMPILE_PHASE | |
| EIGEN_GPU_RUNTIME_CHECK(gpuMemsetAsync(buffer, c, n, stream_->stream())); | |
| #else | |
| EIGEN_UNUSED_VARIABLE(buffer); | |
| EIGEN_UNUSED_VARIABLE(c); | |
| EIGEN_UNUSED_VARIABLE(n); | |
| eigen_assert(false && "The default device should be used instead to generate kernel code"); | |
| #endif | |
| } | |
| template <typename T> | |
| EIGEN_STRONG_INLINE void fill(T* begin, T* end, const T& value) const { | |
| #ifndef EIGEN_GPU_COMPILE_PHASE | |
| const size_t count = end - begin; | |
| // Split value into bytes and run memset with stride. | |
| const int value_size = sizeof(value); | |
| char* buffer = (char*)begin; | |
| char* value_bytes = (char*)(&value); | |
| // If all value bytes are equal, then a single memset can be much faster. | |
| bool use_single_memset = true; | |
| for (int i = 1; i < value_size; ++i) { | |
| if (value_bytes[i] != value_bytes[0]) { | |
| use_single_memset = false; | |
| } | |
| } | |
| if (use_single_memset) { | |
| EIGEN_GPU_RUNTIME_CHECK(gpuMemsetAsync(buffer, value_bytes[0], count * sizeof(T), stream_->stream())); | |
| } else { | |
| for (int b = 0; b < value_size; ++b) { | |
| EIGEN_GPU_RUNTIME_CHECK(gpuMemset2DAsync(buffer + b, value_size, value_bytes[b], 1, count, stream_->stream())); | |
| } | |
| } | |
| #else | |
| EIGEN_UNUSED_VARIABLE(begin); | |
| EIGEN_UNUSED_VARIABLE(end); | |
| EIGEN_UNUSED_VARIABLE(value); | |
| eigen_assert(false && "The default device should be used instead to generate kernel code"); | |
| #endif | |
| } | |
| // The warp (wavefront) width, which is what "a thread" means to the block-size heuristics that ask. | |
| EIGEN_STRONG_INLINE size_t numThreads() const { return static_cast<size_t>(warpSize()); } | |
| EIGEN_STRONG_INLINE size_t firstLevelCacheSize() const { | |
| // FIXME: Return a more accurate cache size. | |
| return 48 * 1024; | |
| } | |
| EIGEN_STRONG_INLINE size_t lastLevelCacheSize() const { | |
| // We won't try to take advantage of the l2 cache for the time being, and | |
| // there is no l3 cache on hip/cuda devices. | |
| return firstLevelCacheSize(); | |
| } | |
| EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void synchronize() const { | |
| #ifndef EIGEN_GPU_COMPILE_PHASE | |
| EIGEN_GPU_RUNTIME_CHECK(gpuStreamSynchronize(stream_->stream())); | |
| #else | |
| gpu_assert(false && "The default device should be used instead to generate kernel code"); | |
| #endif | |
| } | |
| EIGEN_STRONG_INLINE int getNumGpuMultiProcessors() const { return stream_->deviceProperties().multiProcessorCount; } | |
| EIGEN_STRONG_INLINE int maxGpuThreadsPerBlock() const { return stream_->deviceProperties().maxThreadsPerBlock; } | |
| EIGEN_STRONG_INLINE int maxGpuThreadsPerMultiProcessor() const { | |
| return stream_->deviceProperties().maxThreadsPerMultiProcessor; | |
| } | |
| EIGEN_STRONG_INLINE int sharedMemPerBlock() const { | |
| return static_cast<int>(stream_->deviceProperties().sharedMemPerBlock); | |
| } | |
| EIGEN_STRONG_INLINE int majorDeviceVersion() const { return stream_->deviceProperties().major; } | |
| EIGEN_STRONG_INLINE int minorDeviceVersion() const { return stream_->deviceProperties().minor; } | |
| // Read from the StreamInterface's attributes rather than from its properties: gpuDeviceProp_t does not carry | |
| // the opt-in shared-memory limit or memory-pool support portably. Like the properties above, they describe the | |
| // device the stream and its allocations belong to, which need not be the device the calling thread is bound to. | |
| EIGEN_STRONG_INLINE int warpSize() const { return stream_->deviceAttributes().warpSize; } | |
| // The shared memory a kernel may request with gpuFuncSetAttribute, or the ordinary block limit when HIP does | |
| // not implement the opt-in attribute. | |
| EIGEN_STRONG_INLINE int sharedMemPerBlockOptin() const { return stream_->deviceAttributes().sharedMemPerBlockOptin; } | |
| // Whether gpuMallocAsync and its pool are available on this device and driver. | |
| EIGEN_STRONG_INLINE bool memoryPoolsSupported() const { | |
| return stream_->deviceAttributes().memoryPoolsSupported != 0; | |
| } | |
| EIGEN_DEPRECATED EIGEN_STRONG_INLINE int maxBlocks() const { return max_blocks_; } | |
| // This function checks if the GPU runtime recorded an error for the | |
| // underlying stream device. | |
| inline bool ok() const { | |
| #ifdef EIGEN_GPUCC | |
| gpuError_t error = gpuStreamQuery(stream_->stream()); | |
| return (error == gpuSuccess) || (error == gpuErrorNotReady); | |
| #else | |
| return false; | |
| #endif | |
| } | |
| private: | |
| const StreamInterface* stream_; | |
| int max_blocks_; | |
| }; | |
| // Launches `kernel` on the device's stream through internal::gpu_launch (GpuRuntime.h), which reports a failed | |
| // launch through EIGEN_GPU_RUNTIME_CHECK. A single statement, so it composes with an unbraced `if`. | |
| #define LAUNCH_GPU_KERNEL(kernel, gridsize, blocksize, sharedmem, device, ...) \ | |
| do { \ | |
| ::Eigen::internal::gpu_launch((kernel), dim3(gridsize), dim3(blocksize), (sharedmem), (device).stream(), \ | |
| __VA_ARGS__); \ | |
| } while (0) | |
| } // end namespace Eigen | |
| // undefine all the gpu* macros we defined at the beginning of the file | |
| #include "../../../../Eigen/src/Core/util/GpuHipCudaUndefines.inc" | |
| #endif // EIGEN_TENSOR_TENSOR_DEVICE_GPU_H |