Skip to content

Commit b1f5da4

Browse files
[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.
1 parent 831a154 commit b1f5da4

13 files changed

+744
-114
lines changed

libc/benchmarks/gpu/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ add_unittest_framework_library(
4040
LibcGpuBenchmarkMain.cpp
4141
HDRS
4242
LibcGpuBenchmark.h
43+
Random.h
4344
DEPENDS
4445
libc.benchmarks.gpu.timing.timing
4546
libc.hdr.stdint_proxy
@@ -49,12 +50,17 @@ add_unittest_framework_library(
4950
libc.src.__support.CPP.algorithm
5051
libc.src.__support.CPP.atomic
5152
libc.src.__support.CPP.array
53+
libc.src.__support.CPP.optional
5254
libc.src.__support.FPUtil.fp_bits
5355
libc.src.__support.FPUtil.nearest_integer_operations
5456
libc.src.__support.FPUtil.sqrt
57+
libc.src.__support.sign
5558
libc.src.__support.fixedvector
5659
libc.src.__support.GPU.utils
5760
libc.src.__support.time.gpu.time_utils
61+
libc.src.__support.macros.attributes
62+
libc.src.__support.macros.config
63+
libc.src.__support.macros.properties.types
5864
libc.src.stdio.printf
5965
libc.src.time.clock
6066
)

libc/benchmarks/gpu/LibcGpuBenchmark.h

Lines changed: 11 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#ifndef LLVM_LIBC_BENCHMARKS_LIBC_GPU_BENCHMARK_H
22
#define LLVM_LIBC_BENCHMARKS_LIBC_GPU_BENCHMARK_H
33

4+
#include "benchmarks/gpu/Random.h"
5+
46
#include "benchmarks/gpu/timing/timing.h"
57

68
#include "hdr/stdint_proxy.h"
@@ -175,94 +177,6 @@ class Benchmark {
175177
}
176178
};
177179

178-
class RandomGenerator {
179-
uint64_t state;
180-
181-
static LIBC_INLINE uint64_t splitmix64(uint64_t x) noexcept {
182-
x += 0x9E3779B97F4A7C15ULL;
183-
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
184-
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
185-
x = (x ^ (x >> 31));
186-
return x ? x : 0x9E3779B97F4A7C15ULL;
187-
}
188-
189-
public:
190-
explicit LIBC_INLINE RandomGenerator(uint64_t seed) noexcept
191-
: state(splitmix64(seed)) {}
192-
193-
LIBC_INLINE uint64_t next64() noexcept {
194-
uint64_t x = state;
195-
x ^= x >> 12;
196-
x ^= x << 25;
197-
x ^= x >> 27;
198-
state = x;
199-
return x * 0x2545F4914F6CDD1DULL;
200-
}
201-
202-
LIBC_INLINE uint32_t next32() noexcept {
203-
return static_cast<uint32_t>(next64() >> 32);
204-
}
205-
};
206-
207-
// We want random floating-point values whose *unbiased* exponent e is
208-
// approximately uniform in [min_exp, max_exp]. That is,
209-
// 2^min_exp <= |value| < 2^(max_exp + 1).
210-
// Caveats / boundaries:
211-
// - e = -EXP_BIAS ==> subnormal range (biased exponent = 0). We ensure a
212-
// non-zero mantissa so we don't accidentally produce 0.
213-
// - e in [1 - EXP_BIAS, EXP_BIAS] ==> normal numbers.
214-
// - e = EXP_BIAS + 1 ==> Inf/NaN. We do not include it by default; max_exp
215-
// defaults to EXP_BIAS.
216-
template <typename T>
217-
static T
218-
get_rand_input(RandomGenerator &rng,
219-
int min_exp = -LIBC_NAMESPACE::fputil::FPBits<T>::EXP_BIAS,
220-
int max_exp = LIBC_NAMESPACE::fputil::FPBits<T>::EXP_BIAS) {
221-
using FPBits = LIBC_NAMESPACE::fputil::FPBits<T>;
222-
using Storage = typename FPBits::StorageType;
223-
224-
// Sanitize and clamp requested range to what the format supports
225-
if (min_exp > max_exp) {
226-
auto tmp = min_exp;
227-
min_exp = max_exp;
228-
max_exp = tmp;
229-
};
230-
min_exp = cpp::max(min_exp, -FPBits::EXP_BIAS);
231-
max_exp = cpp::min(max_exp, FPBits::EXP_BIAS);
232-
233-
// Sample unbiased exponent e uniformly in [min_exp, max_exp] without modulo
234-
// bias
235-
auto sample_in_range = [&](uint64_t r) -> int32_t {
236-
const uint64_t range = static_cast<uint64_t>(
237-
static_cast<int64_t>(max_exp) - static_cast<int64_t>(min_exp) + 1);
238-
const uint64_t threshold = (-range) % range;
239-
while (r < threshold)
240-
r = rng.next64();
241-
return static_cast<int32_t>(min_exp + static_cast<int64_t>(r % range));
242-
};
243-
const int32_t e = sample_in_range(rng.next64());
244-
245-
// Start from random bits to get random sign and mantissa
246-
FPBits xbits([&] {
247-
if constexpr (cpp::is_same_v<T, double>)
248-
return FPBits(rng.next64());
249-
else
250-
return FPBits(rng.next32());
251-
}());
252-
253-
if (e == -FPBits::EXP_BIAS) {
254-
// Subnormal: biased exponent must be 0; ensure mantissa != 0 to avoid 0
255-
xbits.set_biased_exponent(Storage(0));
256-
if (xbits.get_mantissa() == Storage(0))
257-
xbits.set_mantissa(Storage(1));
258-
} else {
259-
// Normal: biased exponent in [1, 2 * FPBits::EXP_BIAS]
260-
const int32_t biased = e + FPBits::EXP_BIAS;
261-
xbits.set_biased_exponent(static_cast<Storage>(biased));
262-
}
263-
return xbits.get_val();
264-
}
265-
266180
template <typename T> class MathPerf {
267181
static LIBC_INLINE uint64_t make_seed(uint64_t base_seed, uint64_t salt) {
268182
const uint64_t tid = gpu::get_thread_id();
@@ -271,29 +185,27 @@ template <typename T> class MathPerf {
271185

272186
public:
273187
// Returns cycles-per-call (lower is better)
274-
template <size_t N = 1>
275-
static uint64_t run_throughput_in_range(T f(T), int min_exp, int max_exp,
276-
uint32_t call_index) {
188+
template <size_t N = 1, typename Dist>
189+
static uint64_t run_throughput(T (*f)(T), const Dist &dist,
190+
uint32_t call_index) {
277191
cpp::array<T, N> inputs;
278192

279193
uint64_t base_seed = static_cast<uint64_t>(call_index);
280194
uint64_t salt = static_cast<uint64_t>(N);
281195
RandomGenerator rng(make_seed(base_seed, salt));
282196

283197
for (size_t i = 0; i < N; ++i)
284-
inputs[i] = get_rand_input<T>(rng, min_exp, max_exp);
198+
inputs[i] = dist(rng);
285199

286200
uint64_t total_time = LIBC_NAMESPACE::throughput(f, inputs);
287201

288202
return total_time / N;
289203
}
290204

291205
// Returns cycles-per-call (lower is better)
292-
template <size_t N = 1>
293-
static uint64_t run_throughput_in_range(T f(T, T), int arg1_min_exp,
294-
int arg1_max_exp, int arg2_min_exp,
295-
int arg2_max_exp,
296-
uint32_t call_index) {
206+
template <size_t N = 1, typename Dist1, typename Dist2>
207+
static uint64_t run_throughput(T (*f)(T, T), const Dist1 &dist1,
208+
const Dist2 &dist2, uint32_t call_index) {
297209
cpp::array<T, N> inputs1;
298210
cpp::array<T, N> inputs2;
299211

@@ -302,8 +214,8 @@ template <typename T> class MathPerf {
302214
RandomGenerator rng(make_seed(base_seed, salt));
303215

304216
for (size_t i = 0; i < N; ++i) {
305-
inputs1[i] = get_rand_input<T>(rng, arg1_min_exp, arg1_max_exp);
306-
inputs2[i] = get_rand_input<T>(rng, arg2_min_exp, arg2_max_exp);
217+
inputs1[i] = dist1(rng);
218+
inputs2[i] = dist2(rng);
307219
}
308220

309221
uint64_t total_time = LIBC_NAMESPACE::throughput(f, inputs1, inputs2);

libc/benchmarks/gpu/Random.h

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
//===-- Pseudo-random number generation utilities ---------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_LIBC_BENCHMARKS_GPU_RANDOM_H
10+
#define LLVM_LIBC_BENCHMARKS_GPU_RANDOM_H
11+
12+
#include "hdr/stdint_proxy.h"
13+
#include "src/__support/CPP/algorithm.h"
14+
#include "src/__support/CPP/optional.h"
15+
#include "src/__support/CPP/type_traits.h"
16+
#include "src/__support/FPUtil/FPBits.h"
17+
#include "src/__support/macros/attributes.h"
18+
#include "src/__support/macros/config.h"
19+
#include "src/__support/macros/properties/types.h"
20+
#include "src/__support/sign.h"
21+
22+
namespace LIBC_NAMESPACE_DECL {
23+
namespace benchmarks {
24+
25+
// Pseudo-random number generator (PRNG) that produces unsigned 64-bit, 32-bit,
26+
// and 16-bit integers. The implementation is based on the xorshift* generator,
27+
// seeded using SplitMix64 for robust initialization. For more details, see:
28+
// https://en.wikipedia.org/wiki/Xorshift
29+
class RandomGenerator {
30+
uint64_t state;
31+
32+
static LIBC_INLINE uint64_t splitmix64(uint64_t x) noexcept {
33+
x += 0x9E3779B97F4A7C15ULL;
34+
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
35+
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
36+
x = (x ^ (x >> 31));
37+
return x ? x : 0x9E3779B97F4A7C15ULL;
38+
}
39+
40+
public:
41+
explicit LIBC_INLINE RandomGenerator(uint64_t seed) noexcept
42+
: state(splitmix64(seed)) {}
43+
44+
LIBC_INLINE uint64_t next64() noexcept {
45+
uint64_t x = state;
46+
x ^= x >> 12;
47+
x ^= x << 25;
48+
x ^= x >> 27;
49+
state = x;
50+
return x * 0x2545F4914F6CDD1DULL;
51+
}
52+
53+
LIBC_INLINE uint32_t next32() noexcept {
54+
return static_cast<uint32_t>(next64() >> 32);
55+
}
56+
57+
LIBC_INLINE uint16_t next16() noexcept {
58+
return static_cast<uint16_t>(next64() >> 48);
59+
}
60+
};
61+
62+
// Generates random floating-point numbers where the unbiased binary exponent
63+
// is sampled uniformly in `[min_exp, max_exp]`. The significand bits are
64+
// always randomized, while the sign is randomized by default but can be fixed.
65+
// Evenly covers orders of magnitude; never yields Inf/NaN.
66+
template <typename T> class UniformExponent {
67+
static_assert(cpp::is_same_v<T, float16> || cpp::is_same_v<T, float> ||
68+
cpp::is_same_v<T, double>,
69+
"UniformExponent supports float16, float, and double");
70+
71+
using FPBits = LIBC_NAMESPACE::fputil::FPBits<T>;
72+
using Storage = typename FPBits::StorageType;
73+
74+
public:
75+
explicit UniformExponent(int min_exp = -FPBits::EXP_BIAS,
76+
int max_exp = FPBits::EXP_BIAS,
77+
cpp::optional<Sign> forced_sign = cpp::nullopt)
78+
: min_exp(clamp_exponent(cpp::min(min_exp, max_exp))),
79+
max_exp(clamp_exponent(cpp::max(min_exp, max_exp))),
80+
forced_sign(forced_sign) {}
81+
82+
LIBC_INLINE T operator()(RandomGenerator &rng) const noexcept {
83+
// Sample unbiased exponent e uniformly in [min_exp, max_exp] without modulo
84+
// bias, using rejection sampling
85+
auto sample_in_range = [&](uint64_t r) -> int32_t {
86+
const uint64_t range = static_cast<uint64_t>(
87+
static_cast<int64_t>(max_exp) - static_cast<int64_t>(min_exp) + 1);
88+
const uint64_t threshold = (-range) % range;
89+
while (r < threshold)
90+
r = rng.next64();
91+
return static_cast<int32_t>(min_exp + static_cast<int64_t>(r % range));
92+
};
93+
const int32_t e = sample_in_range(rng.next64());
94+
95+
// Start from random bits to get random sign and mantissa
96+
FPBits xbits([&] {
97+
if constexpr (cpp::is_same_v<T, double>)
98+
return FPBits(rng.next64());
99+
else if constexpr (cpp::is_same_v<T, float>)
100+
return FPBits(rng.next32());
101+
else
102+
return FPBits(rng.next16());
103+
}());
104+
105+
if (e == -FPBits::EXP_BIAS) {
106+
// Subnormal: biased exponent must be 0; ensure mantissa != 0 to avoid 0
107+
xbits.set_biased_exponent(Storage(0));
108+
if (xbits.get_mantissa() == Storage(0))
109+
xbits.set_mantissa(Storage(1));
110+
} else {
111+
// Normal: biased exponent in [1, 2 * FPBits::EXP_BIAS]
112+
const int32_t biased = e + FPBits::EXP_BIAS;
113+
xbits.set_biased_exponent(static_cast<Storage>(biased));
114+
}
115+
116+
if (forced_sign)
117+
xbits.set_sign(*forced_sign);
118+
119+
return xbits.get_val();
120+
}
121+
122+
private:
123+
static LIBC_INLINE int clamp_exponent(int val) noexcept {
124+
if (val < -FPBits::EXP_BIAS)
125+
return -FPBits::EXP_BIAS;
126+
127+
if (val > FPBits::EXP_BIAS)
128+
return FPBits::EXP_BIAS;
129+
130+
return val;
131+
}
132+
133+
const int min_exp;
134+
const int max_exp;
135+
const cpp::optional<Sign> forced_sign;
136+
};
137+
138+
// Generates random floating-point numbers that are uniformly distributed on
139+
// a linear scale. Values are sampled from `[min_val, max_val)`.
140+
template <typename T> class UniformLinear {
141+
static_assert(cpp::is_same_v<T, float16> || cpp::is_same_v<T, float> ||
142+
cpp::is_same_v<T, double>,
143+
"UniformLinear supports float16, float, and double");
144+
145+
using FPBits = LIBC_NAMESPACE::fputil::FPBits<T>;
146+
using Storage = typename FPBits::StorageType;
147+
148+
static constexpr T MAX_NORMAL = FPBits::max_normal().get_val();
149+
150+
public:
151+
explicit UniformLinear(T min_val = -MAX_NORMAL, T max_val = MAX_NORMAL)
152+
: min_val(clamp_val(cpp::min(min_val, max_val))),
153+
max_val(clamp_val(cpp::max(min_val, max_val))) {}
154+
155+
LIBC_INLINE T operator()(RandomGenerator &rng) const noexcept {
156+
double u = standard_uniform(rng.next64());
157+
double a = static_cast<double>(min_val);
158+
double b = static_cast<double>(max_val);
159+
double y = a + (b - a) * u;
160+
return static_cast<T>(y);
161+
}
162+
163+
private:
164+
static LIBC_INLINE T clamp_val(T val) noexcept {
165+
if (val < -MAX_NORMAL)
166+
return -MAX_NORMAL;
167+
168+
if (val > MAX_NORMAL)
169+
return MAX_NORMAL;
170+
171+
return val;
172+
}
173+
174+
static LIBC_INLINE double standard_uniform(uint64_t x) noexcept {
175+
constexpr int PREC_BITS =
176+
LIBC_NAMESPACE::fputil::FPBits<double>::SIG_LEN + 1;
177+
constexpr int SHIFT_BITS = LIBC_NAMESPACE::fputil::FPBits<double>::EXP_LEN;
178+
constexpr double INV = 1.0 / static_cast<double>(1ULL << PREC_BITS);
179+
180+
return static_cast<double>(x >> SHIFT_BITS) * INV;
181+
}
182+
183+
const T min_val;
184+
const T max_val;
185+
};
186+
187+
} // namespace benchmarks
188+
} // namespace LIBC_NAMESPACE_DECL
189+
190+
#endif

0 commit comments

Comments
 (0)