This patch adds GPU benchmarks for the exp (`exp`, `expf`, `expf16`) and log (`log`, `logf`, `logf16`) families of math functions. Adding these benchmarks revealed a key limitation in the existing framework: the input generation mechanism was hardcoded to a single strategy that sampled numbers with a uniform distribution of their unbiased exponents. While this strategy is effective for values spanning multiple orders of magnitude, it is not suitable for linear ranges. The previous framework lacked the flexibility to support this. ### Summary of Changes **1. Framework Refactoring for Flexible Input Sampling:** The GPU benchmark framework was refactored to support multiple, pluggable input sampling strategies. * **`Random.h`:** A new header was created to house the `RandomGenerator` and the new distribution classes. * **Distribution Classes:** Two sampling strategies were implemented: * `UniformExponent`: Formalizes the previous logic of sampling numbers with a uniform distribution of their unbiased exponents. It can now also be configured to produce only positive values, which is essential for functions like `log`. * `UniformLinear`: A new strategy that samples numbers from a uniform distribution over a linear interval `[min, max)`. * **`MathPerf` Update:** The `MathPerf` class was updated with a generic `run_throughput` method that is templated on a distribution object. This makes the framework extensible to future sampling strategies. **2. New Benchmarks for `exp` and `log`:** Using the newly refactored framework, benchmarks were added for `exp`, `expf`, `expf16`, `log`, `logf`, and `logf16`. The test intervals were carefully chosen to measure the performance of distinct behavioral regions of each function.
248 lines
7.3 KiB
C++
248 lines
7.3 KiB
C++
#ifndef LLVM_LIBC_BENCHMARKS_LIBC_GPU_BENCHMARK_H
|
|
#define LLVM_LIBC_BENCHMARKS_LIBC_GPU_BENCHMARK_H
|
|
|
|
#include "benchmarks/gpu/Random.h"
|
|
|
|
#include "benchmarks/gpu/timing/timing.h"
|
|
|
|
#include "hdr/stdint_proxy.h"
|
|
#include "src/__support/CPP/algorithm.h"
|
|
#include "src/__support/CPP/array.h"
|
|
#include "src/__support/CPP/string_view.h"
|
|
#include "src/__support/CPP/type_traits.h"
|
|
#include "src/__support/FPUtil/FPBits.h"
|
|
#include "src/__support/FPUtil/sqrt.h"
|
|
#include "src/__support/macros/config.h"
|
|
|
|
namespace LIBC_NAMESPACE_DECL {
|
|
|
|
namespace benchmarks {
|
|
|
|
struct BenchmarkOptions {
|
|
uint32_t initial_iterations = 1;
|
|
uint32_t min_iterations = 1;
|
|
uint32_t max_iterations = 10000000;
|
|
uint32_t min_samples = 4;
|
|
uint32_t max_samples = 1000;
|
|
int64_t min_duration = 500 * 1000; // 500 * 1000 nanoseconds = 500 us
|
|
int64_t max_duration = 1000 * 1000 * 1000; // 1e9 nanoseconds = 1 second
|
|
double epsilon = 0.0001;
|
|
double scaling_factor = 1.4;
|
|
};
|
|
|
|
class RefinableRuntimeEstimator {
|
|
uint32_t iterations = 0;
|
|
uint64_t sum_of_cycles = 0;
|
|
uint64_t sum_of_squared_cycles = 0;
|
|
|
|
public:
|
|
void update(uint64_t cycles) noexcept {
|
|
iterations += 1;
|
|
sum_of_cycles += cycles;
|
|
sum_of_squared_cycles += cycles * cycles;
|
|
}
|
|
|
|
void update(const RefinableRuntimeEstimator &other) noexcept {
|
|
iterations += other.iterations;
|
|
sum_of_cycles += other.sum_of_cycles;
|
|
sum_of_squared_cycles += other.sum_of_squared_cycles;
|
|
}
|
|
|
|
double get_mean() const noexcept {
|
|
if (iterations == 0)
|
|
return 0.0;
|
|
|
|
return static_cast<double>(sum_of_cycles) / iterations;
|
|
}
|
|
|
|
double get_variance() const noexcept {
|
|
if (iterations == 0)
|
|
return 0.0;
|
|
|
|
const double num = static_cast<double>(iterations);
|
|
const double sum_x = static_cast<double>(sum_of_cycles);
|
|
const double sum_x2 = static_cast<double>(sum_of_squared_cycles);
|
|
|
|
const double mean_of_squares = sum_x2 / num;
|
|
const double mean = sum_x / num;
|
|
const double mean_squared = mean * mean;
|
|
const double variance = mean_of_squares - mean_squared;
|
|
|
|
return variance < 0.0 ? 0.0 : variance;
|
|
}
|
|
|
|
double get_stddev() const noexcept {
|
|
return fputil::sqrt<double>(get_variance());
|
|
}
|
|
|
|
uint32_t get_iterations() const noexcept { return iterations; }
|
|
};
|
|
|
|
// Tracks the progression of the runtime estimation
|
|
class RuntimeEstimationProgression {
|
|
RefinableRuntimeEstimator estimator;
|
|
double current_mean = 0.0;
|
|
|
|
public:
|
|
const RefinableRuntimeEstimator &get_estimator() const noexcept {
|
|
return estimator;
|
|
}
|
|
|
|
double
|
|
compute_improvement(const RefinableRuntimeEstimator &sample_estimator) {
|
|
if (sample_estimator.get_iterations() == 0)
|
|
return 1.0;
|
|
|
|
estimator.update(sample_estimator);
|
|
|
|
const double new_mean = estimator.get_mean();
|
|
if (current_mean == 0.0 || new_mean == 0.0) {
|
|
current_mean = new_mean;
|
|
return 1.0;
|
|
}
|
|
|
|
double ratio = (current_mean / new_mean) - 1.0;
|
|
if (ratio < 0)
|
|
ratio = -ratio;
|
|
|
|
current_mean = new_mean;
|
|
return ratio;
|
|
}
|
|
};
|
|
|
|
struct BenchmarkResult {
|
|
uint64_t total_iterations = 0;
|
|
double cycles = 0;
|
|
double standard_deviation = 0;
|
|
uint64_t min = UINT64_MAX;
|
|
uint64_t max = 0;
|
|
};
|
|
|
|
struct BenchmarkTarget {
|
|
using IndexedFnPtr = uint64_t (*)(uint32_t);
|
|
using IndexlessFnPtr = uint64_t (*)();
|
|
|
|
enum class Kind : uint8_t { Indexed, Indexless } kind;
|
|
union {
|
|
IndexedFnPtr indexed_fn_ptr;
|
|
IndexlessFnPtr indexless_fn_ptr;
|
|
};
|
|
|
|
LIBC_INLINE BenchmarkTarget(IndexedFnPtr func)
|
|
: kind(Kind::Indexed), indexed_fn_ptr(func) {}
|
|
LIBC_INLINE BenchmarkTarget(IndexlessFnPtr func)
|
|
: kind(Kind::Indexless), indexless_fn_ptr(func) {}
|
|
|
|
LIBC_INLINE uint64_t operator()([[maybe_unused]] uint32_t call_index) const {
|
|
return kind == Kind::Indexed ? indexed_fn_ptr(call_index)
|
|
: indexless_fn_ptr();
|
|
}
|
|
};
|
|
|
|
BenchmarkResult benchmark(const BenchmarkOptions &options,
|
|
const BenchmarkTarget &target);
|
|
|
|
class Benchmark {
|
|
const BenchmarkTarget target;
|
|
const cpp::string_view suite_name;
|
|
const cpp::string_view test_name;
|
|
const uint32_t num_threads;
|
|
|
|
public:
|
|
Benchmark(uint64_t (*f)(), const char *suite, const char *test,
|
|
uint32_t threads)
|
|
: target(BenchmarkTarget(f)), suite_name(suite), test_name(test),
|
|
num_threads(threads) {
|
|
add_benchmark(this);
|
|
}
|
|
|
|
Benchmark(uint64_t (*f)(uint32_t), char const *suite_name,
|
|
char const *test_name, uint32_t num_threads)
|
|
: target(BenchmarkTarget(f)), suite_name(suite_name),
|
|
test_name(test_name), num_threads(num_threads) {
|
|
add_benchmark(this);
|
|
}
|
|
|
|
static void run_benchmarks();
|
|
const cpp::string_view get_suite_name() const { return suite_name; }
|
|
const cpp::string_view get_test_name() const { return test_name; }
|
|
|
|
protected:
|
|
static void add_benchmark(Benchmark *benchmark);
|
|
|
|
private:
|
|
BenchmarkResult run() {
|
|
BenchmarkOptions options;
|
|
return benchmark(options, target);
|
|
}
|
|
};
|
|
|
|
template <typename T> class MathPerf {
|
|
static LIBC_INLINE uint64_t make_seed(uint64_t base_seed, uint64_t salt) {
|
|
const uint64_t tid = gpu::get_thread_id();
|
|
return base_seed ^ (salt << 32) ^ (tid * 0x9E3779B97F4A7C15ULL);
|
|
}
|
|
|
|
public:
|
|
// Returns cycles-per-call (lower is better)
|
|
template <size_t N = 1, typename Dist>
|
|
static uint64_t run_throughput(T (*f)(T), const Dist &dist,
|
|
uint32_t call_index) {
|
|
cpp::array<T, N> inputs;
|
|
|
|
uint64_t base_seed = static_cast<uint64_t>(call_index);
|
|
uint64_t salt = static_cast<uint64_t>(N);
|
|
RandomGenerator rng(make_seed(base_seed, salt));
|
|
|
|
for (size_t i = 0; i < N; ++i)
|
|
inputs[i] = dist(rng);
|
|
|
|
uint64_t total_time = LIBC_NAMESPACE::throughput(f, inputs);
|
|
|
|
return total_time / N;
|
|
}
|
|
|
|
// Returns cycles-per-call (lower is better)
|
|
template <size_t N = 1, typename Dist1, typename Dist2>
|
|
static uint64_t run_throughput(T (*f)(T, T), const Dist1 &dist1,
|
|
const Dist2 &dist2, uint32_t call_index) {
|
|
cpp::array<T, N> inputs1;
|
|
cpp::array<T, N> inputs2;
|
|
|
|
uint64_t base_seed = static_cast<uint64_t>(call_index);
|
|
uint64_t salt = static_cast<uint64_t>(N);
|
|
RandomGenerator rng(make_seed(base_seed, salt));
|
|
|
|
for (size_t i = 0; i < N; ++i) {
|
|
inputs1[i] = dist1(rng);
|
|
inputs2[i] = dist2(rng);
|
|
}
|
|
|
|
uint64_t total_time = LIBC_NAMESPACE::throughput(f, inputs1, inputs2);
|
|
|
|
return total_time / N;
|
|
}
|
|
};
|
|
|
|
} // namespace benchmarks
|
|
} // namespace LIBC_NAMESPACE_DECL
|
|
|
|
// Passing -1 indicates the benchmark should be run with as many threads as
|
|
// allocated by the user in the benchmark's CMake.
|
|
#define BENCHMARK(SuiteName, TestName, Func) \
|
|
LIBC_NAMESPACE::benchmarks::Benchmark SuiteName##_##TestName##_Instance( \
|
|
Func, #SuiteName, #TestName, -1)
|
|
|
|
#define BENCHMARK_N_THREADS(SuiteName, TestName, Func, NumThreads) \
|
|
LIBC_NAMESPACE::benchmarks::Benchmark SuiteName##_##TestName##_Instance( \
|
|
Func, #SuiteName, #TestName, NumThreads)
|
|
|
|
#define SINGLE_THREADED_BENCHMARK(SuiteName, TestName, Func) \
|
|
BENCHMARK_N_THREADS(SuiteName, TestName, Func, 1)
|
|
|
|
#define SINGLE_WAVE_BENCHMARK(SuiteName, TestName, Func) \
|
|
BENCHMARK_N_THREADS(SuiteName, TestName, Func, \
|
|
LIBC_NAMESPACE::gpu::get_lane_size())
|
|
|
|
#endif // LLVM_LIBC_BENCHMARKS_LIBC_GPU_BENCHMARK_H
|