| // This file is part of Eigen, a lightweight C++ template library | |
| // for linear algebra. | |
| // | |
| // 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-FileCopyrightText: The Eigen Authors | |
| // SPDX-License-Identifier: MPL-2.0 | |
| #ifndef EIGEN_BENCHMARKS_GPU_BENCH_COMMON_H | |
| #define EIGEN_BENCHMARKS_GPU_BENCH_COMMON_H | |
| // Shared by the benchmarks under benchmarks/GPU: device buffers, | |
| // event-based timing of a batch of launches, result checking outside the timed region, and a main() that records | |
| // the device in the benchmark context. EIGEN_USE_GPU is defined by the CMake target. | |
| #include <benchmark/benchmark.h> | |
| #include <cuda_runtime.h> | |
| #include <chrono> | |
| #include <cmath> | |
| #include <cstdio> | |
| #include <cstdlib> | |
| #include <limits> | |
| #include <string> | |
| #include <vector> | |
| #include <contrib/Eigen/Tensor> | |
| // Every runtime call goes through EIGEN_GPU_RUNTIME_CHECK (Eigen/src/Core/util/GpuRuntime.h, in scope through the | |
| // Tensor header): under the NDEBUG these targets define it reports `file:line: call: name: description` and aborts. | |
| namespace eigen_bench { | |
| // A device allocation of `size` elements, optionally filled from the host with values in [-1, 1]. | |
| template <typename T> | |
| class DeviceBuffer { | |
| public: | |
| explicit DeviceBuffer(size_t size) : size_(size) { | |
| EIGEN_GPU_RUNTIME_CHECK(cudaMalloc(reinterpret_cast<void**>(&data_), size * sizeof(T))); | |
| } | |
| ~DeviceBuffer() { EIGEN_GPU_RUNTIME_CHECK(cudaFree(data_)); } | |
| DeviceBuffer(const DeviceBuffer&) = delete; | |
| DeviceBuffer& operator=(const DeviceBuffer&) = delete; | |
| DeviceBuffer(DeviceBuffer&& other) noexcept : size_(other.size_), data_(other.data_) { | |
| other.size_ = 0; | |
| other.data_ = nullptr; // cudaFree(nullptr) succeeds, so the moved-from destructor is fine | |
| } | |
| T* data() { return data_; } | |
| const T* data() const { return data_; } | |
| size_t size() const { return size_; } | |
| // Values in [-1, 1] from Eigen's own generator, which is deterministic across runs. | |
| void fillRandom() { | |
| const Eigen::Array<T, Eigen::Dynamic, 1> host = Eigen::Array<T, Eigen::Dynamic, 1>::Random(size_); | |
| EIGEN_GPU_RUNTIME_CHECK(cudaMemcpy(data_, host.data(), size_ * sizeof(T), cudaMemcpyHostToDevice)); | |
| } | |
| std::vector<T> toHost() const { | |
| std::vector<T> host(size_); | |
| EIGEN_GPU_RUNTIME_CHECK(cudaMemcpy(host.data(), data_, size_ * sizeof(T), cudaMemcpyDeviceToHost)); | |
| return host; | |
| } | |
| private: | |
| size_t size_; | |
| T* data_ = nullptr; | |
| }; | |
| // Elapsed device time between two events recorded on one stream. | |
| class GpuEventTimer { | |
| public: | |
| GpuEventTimer() { | |
| EIGEN_GPU_RUNTIME_CHECK(cudaEventCreate(&start_)); | |
| EIGEN_GPU_RUNTIME_CHECK(cudaEventCreate(&stop_)); | |
| } | |
| ~GpuEventTimer() { | |
| EIGEN_GPU_RUNTIME_CHECK(cudaEventDestroy(start_)); | |
| EIGEN_GPU_RUNTIME_CHECK(cudaEventDestroy(stop_)); | |
| } | |
| void begin(cudaStream_t stream) { EIGEN_GPU_RUNTIME_CHECK(cudaEventRecord(start_, stream)); } | |
| // Records the end, waits for it and returns the elapsed seconds. | |
| double end(cudaStream_t stream) { | |
| EIGEN_GPU_RUNTIME_CHECK(cudaEventRecord(stop_, stream)); | |
| EIGEN_GPU_RUNTIME_CHECK(cudaEventSynchronize(stop_)); | |
| float ms = 0.0f; | |
| EIGEN_GPU_RUNTIME_CHECK(cudaEventElapsedTime(&ms, start_, stop_)); | |
| return ms * 1e-3; | |
| } | |
| private: | |
| cudaEvent_t start_; | |
| cudaEvent_t stop_; | |
| }; | |
| // Times `enqueue()` on the device: each benchmark iteration records events around `launches_per_iteration` calls | |
| // and reports the device time per call through SetIterationTime, so the registration needs UseManualTime(). Event | |
| // time measures the GPU-side duration including the gaps between launches, not the cost of the API calls; the | |
| // latter is reported separately as the host time per enqueue, measured before the synchronization. Because the | |
| // reported time is per launch, a registration must fix its iteration count (Iterations(n)): left to the | |
| // framework's minimum-time rule, microsecond launches would be iterated hundreds of thousands of times. | |
| template <typename Enqueue> | |
| void timeLaunches(benchmark::State& state, cudaStream_t stream, Enqueue enqueue, int launches_per_iteration) { | |
| GpuEventTimer timer; | |
| double host_seconds = 0.0; | |
| int64_t iterations = 0; | |
| for (auto _ : state) { | |
| timer.begin(stream); | |
| const auto host_begin = std::chrono::steady_clock::now(); | |
| for (int k = 0; k < launches_per_iteration; ++k) enqueue(); | |
| host_seconds += std::chrono::duration<double>(std::chrono::steady_clock::now() - host_begin).count(); | |
| state.SetIterationTime(timer.end(stream) / launches_per_iteration); | |
| ++iterations; | |
| } | |
| state.counters["host_us_per_launch"] = host_seconds * 1e6 / (iterations * launches_per_iteration); | |
| } | |
| // One untimed launch, waited for: module load and any lazy setup stay out of the timed loop, and the result is | |
| // what a correctness check reads. GpuDevice::synchronize() is this same wait on the device's own stream, so a | |
| // benchmark holding a GpuDevice passes device.stream(). | |
| template <typename Enqueue> | |
| void warmUp(cudaStream_t stream, Enqueue enqueue) { | |
| enqueue(); | |
| EIGEN_GPU_RUNTIME_CHECK(cudaStreamSynchronize(stream)); | |
| } | |
| // Bytes moved per launch, as a rate against the manual (device) time. | |
| inline benchmark::Counter bytesPerSecond(double bytes_per_launch) { | |
| return benchmark::Counter(bytes_per_launch, benchmark::Counter::kIsIterationInvariantRate, | |
| benchmark::Counter::kIs1024); | |
| } | |
| // Aborts when two results disagree beyond `tolerance` (relative to the larger magnitude, absolute below 1): a | |
| // faster wrong kernel is not a result. Called once per configuration, outside the timed loop. | |
| template <typename T> | |
| void requireClose(const std::vector<T>& result, const std::vector<T>& reference, double tolerance, const char* what) { | |
| if (result.size() != reference.size()) { | |
| std::fprintf(stderr, "%s: size mismatch %zu vs %zu\n", what, result.size(), reference.size()); | |
| std::abort(); | |
| } | |
| for (size_t i = 0; i < result.size(); ++i) { | |
| const double a = static_cast<double>(result[i]); | |
| const double b = static_cast<double>(reference[i]); | |
| const double scale = std::fmax(1.0, std::fmax(std::fabs(a), std::fabs(b))); | |
| if (!std::isfinite(a) || !std::isfinite(b) || !(std::fabs(a - b) <= tolerance * scale)) { | |
| std::fprintf(stderr, "%s: element %zu is %g, expected %g\n", what, i, a, b); | |
| std::abort(); | |
| } | |
| } | |
| } | |
| // The device a run reports, as the "gpu" context of the JSON output and a line on stdout, since a number without | |
| // the device it was measured on is not a result. | |
| inline void describeDevice() { | |
| int device = 0; | |
| EIGEN_GPU_RUNTIME_CHECK(cudaGetDevice(&device)); | |
| cudaDeviceProp prop; | |
| EIGEN_GPU_RUNTIME_CHECK(cudaGetDeviceProperties(&prop, device)); | |
| int driver = 0, runtime = 0; | |
| EIGEN_GPU_RUNTIME_CHECK(cudaDriverGetVersion(&driver)); | |
| EIGEN_GPU_RUNTIME_CHECK(cudaRuntimeGetVersion(&runtime)); | |
| const std::string capability = std::to_string(prop.major) + "." + std::to_string(prop.minor); | |
| benchmark::AddCustomContext("gpu", prop.name); | |
| benchmark::AddCustomContext("gpu_compute_capability", capability); | |
| benchmark::AddCustomContext("gpu_multiprocessors", std::to_string(prop.multiProcessorCount)); | |
| benchmark::AddCustomContext("cuda_driver_version", std::to_string(driver)); | |
| benchmark::AddCustomContext("cuda_runtime_version", std::to_string(runtime)); | |
| std::printf("GPU: %s, compute capability %s, %d multiprocessors, driver %d, runtime %d\n", prop.name, | |
| capability.c_str(), prop.multiProcessorCount, driver, runtime); | |
| } | |
| inline int benchmarkMain(int argc, char** argv) { | |
| benchmark::Initialize(&argc, argv); | |
| if (benchmark::ReportUnrecognizedArguments(argc, argv)) return 1; | |
| describeDevice(); | |
| benchmark::RunSpecifiedBenchmarks(); | |
| benchmark::Shutdown(); | |
| return 0; | |
| } | |
| } // namespace eigen_bench | |
| // Every GPU benchmark reports device time per launch, so each registration needs manual time and a fixed | |
| // iteration count (see timeLaunches). Both forms take the kIterations, and the sizes form the kSizes, that the | |
| // benchmark file defines. | |
| #define EIGEN_GPU_BENCHMARK(...) BENCHMARK(__VA_ARGS__)->UseManualTime()->Iterations(kIterations) | |
| #define EIGEN_GPU_BENCHMARK_SIZES(...) \ | |
| BENCHMARK_TEMPLATE(__VA_ARGS__)->ArgsProduct({kSizes})->UseManualTime()->Iterations(kIterations) | |
| #define EIGEN_GPU_BENCHMARK_MAIN() \ | |
| int main(int argc, char** argv) { return eigen_bench::benchmarkMain(argc, argv); } | |
| #endif // EIGEN_BENCHMARKS_GPU_BENCH_COMMON_H |