Files
Leandro Lacerda b1f5da4328 [libc][gpu] Add exp/log benchmarks and flexible input generation (#155727)
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.
2025-08-27 21:05:10 -05:00

191 lines
6.2 KiB
C++

//===-- Pseudo-random number generation utilities ---------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef LLVM_LIBC_BENCHMARKS_GPU_RANDOM_H
#define LLVM_LIBC_BENCHMARKS_GPU_RANDOM_H
#include "hdr/stdint_proxy.h"
#include "src/__support/CPP/algorithm.h"
#include "src/__support/CPP/optional.h"
#include "src/__support/CPP/type_traits.h"
#include "src/__support/FPUtil/FPBits.h"
#include "src/__support/macros/attributes.h"
#include "src/__support/macros/config.h"
#include "src/__support/macros/properties/types.h"
#include "src/__support/sign.h"
namespace LIBC_NAMESPACE_DECL {
namespace benchmarks {
// Pseudo-random number generator (PRNG) that produces unsigned 64-bit, 32-bit,
// and 16-bit integers. The implementation is based on the xorshift* generator,
// seeded using SplitMix64 for robust initialization. For more details, see:
// https://en.wikipedia.org/wiki/Xorshift
class RandomGenerator {
uint64_t state;
static LIBC_INLINE uint64_t splitmix64(uint64_t x) noexcept {
x += 0x9E3779B97F4A7C15ULL;
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
x = (x ^ (x >> 31));
return x ? x : 0x9E3779B97F4A7C15ULL;
}
public:
explicit LIBC_INLINE RandomGenerator(uint64_t seed) noexcept
: state(splitmix64(seed)) {}
LIBC_INLINE uint64_t next64() noexcept {
uint64_t x = state;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
state = x;
return x * 0x2545F4914F6CDD1DULL;
}
LIBC_INLINE uint32_t next32() noexcept {
return static_cast<uint32_t>(next64() >> 32);
}
LIBC_INLINE uint16_t next16() noexcept {
return static_cast<uint16_t>(next64() >> 48);
}
};
// Generates random floating-point numbers where the unbiased binary exponent
// is sampled uniformly in `[min_exp, max_exp]`. The significand bits are
// always randomized, while the sign is randomized by default but can be fixed.
// Evenly covers orders of magnitude; never yields Inf/NaN.
template <typename T> class UniformExponent {
static_assert(cpp::is_same_v<T, float16> || cpp::is_same_v<T, float> ||
cpp::is_same_v<T, double>,
"UniformExponent supports float16, float, and double");
using FPBits = LIBC_NAMESPACE::fputil::FPBits<T>;
using Storage = typename FPBits::StorageType;
public:
explicit UniformExponent(int min_exp = -FPBits::EXP_BIAS,
int max_exp = FPBits::EXP_BIAS,
cpp::optional<Sign> forced_sign = cpp::nullopt)
: min_exp(clamp_exponent(cpp::min(min_exp, max_exp))),
max_exp(clamp_exponent(cpp::max(min_exp, max_exp))),
forced_sign(forced_sign) {}
LIBC_INLINE T operator()(RandomGenerator &rng) const noexcept {
// Sample unbiased exponent e uniformly in [min_exp, max_exp] without modulo
// bias, using rejection sampling
auto sample_in_range = [&](uint64_t r) -> int32_t {
const uint64_t range = static_cast<uint64_t>(
static_cast<int64_t>(max_exp) - static_cast<int64_t>(min_exp) + 1);
const uint64_t threshold = (-range) % range;
while (r < threshold)
r = rng.next64();
return static_cast<int32_t>(min_exp + static_cast<int64_t>(r % range));
};
const int32_t e = sample_in_range(rng.next64());
// Start from random bits to get random sign and mantissa
FPBits xbits([&] {
if constexpr (cpp::is_same_v<T, double>)
return FPBits(rng.next64());
else if constexpr (cpp::is_same_v<T, float>)
return FPBits(rng.next32());
else
return FPBits(rng.next16());
}());
if (e == -FPBits::EXP_BIAS) {
// Subnormal: biased exponent must be 0; ensure mantissa != 0 to avoid 0
xbits.set_biased_exponent(Storage(0));
if (xbits.get_mantissa() == Storage(0))
xbits.set_mantissa(Storage(1));
} else {
// Normal: biased exponent in [1, 2 * FPBits::EXP_BIAS]
const int32_t biased = e + FPBits::EXP_BIAS;
xbits.set_biased_exponent(static_cast<Storage>(biased));
}
if (forced_sign)
xbits.set_sign(*forced_sign);
return xbits.get_val();
}
private:
static LIBC_INLINE int clamp_exponent(int val) noexcept {
if (val < -FPBits::EXP_BIAS)
return -FPBits::EXP_BIAS;
if (val > FPBits::EXP_BIAS)
return FPBits::EXP_BIAS;
return val;
}
const int min_exp;
const int max_exp;
const cpp::optional<Sign> forced_sign;
};
// Generates random floating-point numbers that are uniformly distributed on
// a linear scale. Values are sampled from `[min_val, max_val)`.
template <typename T> class UniformLinear {
static_assert(cpp::is_same_v<T, float16> || cpp::is_same_v<T, float> ||
cpp::is_same_v<T, double>,
"UniformLinear supports float16, float, and double");
using FPBits = LIBC_NAMESPACE::fputil::FPBits<T>;
using Storage = typename FPBits::StorageType;
static constexpr T MAX_NORMAL = FPBits::max_normal().get_val();
public:
explicit UniformLinear(T min_val = -MAX_NORMAL, T max_val = MAX_NORMAL)
: min_val(clamp_val(cpp::min(min_val, max_val))),
max_val(clamp_val(cpp::max(min_val, max_val))) {}
LIBC_INLINE T operator()(RandomGenerator &rng) const noexcept {
double u = standard_uniform(rng.next64());
double a = static_cast<double>(min_val);
double b = static_cast<double>(max_val);
double y = a + (b - a) * u;
return static_cast<T>(y);
}
private:
static LIBC_INLINE T clamp_val(T val) noexcept {
if (val < -MAX_NORMAL)
return -MAX_NORMAL;
if (val > MAX_NORMAL)
return MAX_NORMAL;
return val;
}
static LIBC_INLINE double standard_uniform(uint64_t x) noexcept {
constexpr int PREC_BITS =
LIBC_NAMESPACE::fputil::FPBits<double>::SIG_LEN + 1;
constexpr int SHIFT_BITS = LIBC_NAMESPACE::fputil::FPBits<double>::EXP_LEN;
constexpr double INV = 1.0 / static_cast<double>(1ULL << PREC_BITS);
return static_cast<double>(x >> SHIFT_BITS) * INV;
}
const T min_val;
const T max_val;
};
} // namespace benchmarks
} // namespace LIBC_NAMESPACE_DECL
#endif