libdpf/test/profile/grotto_profile.cpp

510 lines
21 KiB
C++
Raw Normal View History

/// @file test/profile/grotto_profile.cpp
/// @brief Workload for grotto evaluation: tables, offset walks, prefix parity.
///
/// Key generation is a separate case from the eval that consumes those keys.
/// Table cases probe the domain once, then time only inputs that evaluate.
/// `items` is the number of table evaluations inside one sample, or 1 for a
/// single offset / prefix walk (both parties).
///
/// profile_grotto --list
/// profile_grotto --case window_gelu_p16 --repeat 100
///
/// Profile-guided build, separate from COVERAGE:
/// cmake -S test -B build-pgo -DLIBDPF_PGO=generate -DCMAKE_BUILD_TYPE=Release
/// cmake --build build-pgo --target profile_grotto
/// build-pgo/bin/profile_grotto --repeat 40 --warmup 2
/// cmake -S test -B build-pgo -DLIBDPF_PGO=use -DCMAKE_BUILD_TYPE=Release
/// cmake --build build-pgo --target profile_grotto
#include "harness.hpp"
#include "dpf.hpp"
#include "grotto/closed_form.hpp"
#include "grotto/exact_steps.hpp"
#include "grotto/offset_horner.hpp"
#include "grotto/offset_jet.hpp"
#include "grotto/offset_poly.hpp"
#include "grotto/offset_repr.hpp"
#include "grotto/offset_twist.hpp"
#include "grotto/prefix_parity.hpp"
#include "grotto/range_lut.hpp"
#include "grotto/window_lut.hpp"
#include <array>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <memory>
#include <string>
#include <vector>
namespace
{
using profile::sample;
using profile::touch_arr;
using profile::touch_vec;
using profile::touch_word;
using profile::work;
const char kUsage[] =
"profile_grotto [--list] [--family grotto] [--slice S] [--case NAME]\n"
" [--repeat N=30] [--warmup W=2]\n"
"Slices: window, principal, closed, reduced, exact, horner, poly, jet,\n"
" twist, repr, prefix, keygen.\n"
"Times grotto table evals and offset / prefix walks.\n";
inline work cell(const char * slice, std::string name, std::uint64_t items,
std::function<sample()> fn)
{
return profile::make_work("grotto", slice, std::move(name), items, std::move(fn));
}
template <typename Enum, typename Fn>
std::vector<std::int64_t> probe(Enum which, unsigned bits, Fn fn,
const std::vector<std::int64_t> & cand)
{
std::vector<std::int64_t> ok;
ok.reserve(cand.size());
for (const std::int64_t raw : cand)
{
try
{
(void)fn(which, bits, raw);
ok.push_back(raw);
}
catch (const std::exception &)
{
}
}
return ok;
}
std::vector<std::int64_t> around_one(unsigned bits)
{
const std::int64_t one = std::int64_t{1} << bits;
return {
-4 * one, -2 * one, -one, -one / 2, -1, 0, 1,
one / 20, one / 5, one / 3, one / 2, one, (3 * one) / 2, 2 * one, 4 * one
};
}
template <typename Fn>
void add_table(std::vector<work> & works, const char * slice, std::string name, Fn fn,
std::vector<std::int64_t> samples)
{
if (samples.empty())
{
std::cerr << name << " skipped: no in-domain sample\n";
return;
}
auto held = std::make_shared<std::vector<std::int64_t>>(std::move(samples));
const std::uint64_t items = static_cast<std::uint64_t>(held->size()) * 32u;
std::function<sample()> body = [held, fn]() -> sample {
std::uint64_t h = 0x84222325cbf29ce4ull;
for (int rep = 0; rep < 32; ++rep)
{
for (const std::int64_t raw : *held)
{
// Multiply-add, not a plain xor: an even number of identical
// xors cancels, and the compiler will delete the evals.
h = (h * 0x100000001b3ull) ^ static_cast<std::uint64_t>(fn(raw))
^ static_cast<std::uint64_t>(rep);
}
}
return touch_word(h, held->size() * sizeof(std::int64_t));
};
works.push_back(cell(slice, std::move(name), items, std::move(body)));
}
template <typename T>
std::vector<T> knots_of(std::size_t n, T gap)
{
std::vector<T> knots(n);
for (std::size_t i = 0; i < n; ++i)
knots[i] = static_cast<T>(1 + gap * static_cast<T>(i));
return knots;
}
template <std::size_t Degree>
std::vector<std::array<std::uint64_t, Degree + 1>> horner_rows(std::size_t n)
{
std::vector<std::array<std::uint64_t, Degree + 1>> rows(n);
for (std::size_t i = 0; i < n; ++i)
for (std::size_t m = 0; m <= Degree; ++m)
rows[i][m] = (i + 1) * (m + 3);
return rows;
}
std::vector<std::vector<std::uint64_t>> poly_rows(std::size_t n, std::size_t degree)
{
std::vector<std::vector<std::uint64_t>> rows(n, std::vector<std::uint64_t>(degree + 1));
for (std::size_t i = 0; i < n; ++i)
for (std::size_t m = 0; m <= degree; ++m)
rows[i][m] = (i + 1) * (m + 3);
return rows;
}
std::vector<std::uint64_t> coeff_col(std::size_t degree)
{
std::vector<std::uint64_t> c(degree + 1);
for (std::size_t m = 0; m <= degree; ++m)
c[m] = m + 5;
return c;
}
template <std::size_t N, typename T>
std::array<T, N> ends_of(T start, T step)
{
std::array<T, N> ends{};
for (std::size_t i = 0; i < N; ++i)
ends[i] = static_cast<T>(start + step * static_cast<T>(i));
return ends;
}
} // namespace
template <std::size_t Degree>
void add_horner_degree(std::vector<work> & works)
{
using in_t = std::uint32_t;
const in_t center = 50;
const in_t eta = 7;
works.push_back(cell("keygen", "horner_keygen_d" + std::to_string(Degree), 1, [] {
auto built = grotto::make_offset_horner_keys<in_t, Degree>(in_t{50});
return touch_word(built.wrap_share[0][0] ^ built.wrap_share[Degree][1],
sizeof(built.wrap_share));
}));
using mat_t = decltype(grotto::make_offset_horner_keys<in_t, Degree>(center));
auto mat = std::make_shared<mat_t>(grotto::make_offset_horner_keys<in_t, Degree>(center));
for (const std::size_t pieces : std::initializer_list<std::size_t>{1, 4, 16, 64})
{
auto knots = std::make_shared<std::vector<in_t>>(knots_of<in_t>(pieces, 1000));
auto rows = std::make_shared<std::vector<std::array<std::uint64_t, Degree + 1>>>(
horner_rows<Degree>(pieces));
const auto name = "horner_eval_d" + std::to_string(Degree)
+ "_p" + std::to_string(pieces);
works.push_back(cell("horner", name, 1, [mat, knots, rows, eta] {
const auto a = grotto::offset_horner_eval<0, Degree>(*mat, *knots, *rows, eta);
const auto b = grotto::offset_horner_eval<1, Degree>(*mat, *knots, *rows, eta);
return touch_word(a ^ b, 16);
}));
}
}
template <typename Input, typename BitPtr, typename GtPtr, std::size_t N>
void add_prefix_n(std::vector<work> & works, const char * width, Input step,
const BitPtr & bits, const GtPtr & gts)
{
const auto ends = ends_of<N>(Input{1}, step);
const auto ntag = std::to_string(N);
works.push_back(cell("prefix", std::string("prefix_") + width + "_e" + ntag, N,
[bits, ends] {
const auto a = grotto::prefix_parities(std::get<0>(*bits), ends);
const auto b = grotto::prefix_parities(std::get<1>(*bits), ends);
return touch_arr(std::get<0>(a)) + touch_arr(std::get<0>(b));
}));
works.push_back(cell("prefix", std::string("signed_prefix_") + width + "_e" + ntag, N,
[gts, ends] {
const auto a = grotto::signed_prefix_parities(std::get<0>(*gts), ends);
const auto b = grotto::signed_prefix_parities(std::get<1>(*gts), ends);
return touch_arr(a) + touch_arr(b);
}));
}
template <typename Input>
void add_prefix_width(std::vector<work> & works, const char * width, Input step)
{
const auto bit_keys = dpf::make_dpf(Input{1000}, dpf::bit{1});
const auto gt_keys = dpf::make_dpf(Input{1000}, dpf::gt(std::uint64_t{1}));
using bit_ptr = std::shared_ptr<std::decay_t<decltype(bit_keys)>>;
using gt_ptr = std::shared_ptr<std::decay_t<decltype(gt_keys)>>;
auto bits = std::make_shared<std::decay_t<decltype(bit_keys)>>(bit_keys);
auto gts = std::make_shared<std::decay_t<decltype(gt_keys)>>(gt_keys);
add_prefix_n<Input, bit_ptr, gt_ptr, 1>(works, width, step, bits, gts);
add_prefix_n<Input, bit_ptr, gt_ptr, 8>(works, width, step, bits, gts);
add_prefix_n<Input, bit_ptr, gt_ptr, 32>(works, width, step, bits, gts);
add_prefix_n<Input, bit_ptr, gt_ptr, 128>(works, width, step, bits, gts);
}
int main(int argc, char ** argv)
{
const auto opt = profile::parse_args(argc, argv, 30, 2, kUsage);
std::vector<work> works;
const unsigned precisions[] = {8u, 16u, 24u, 32u};
const auto add_window = [&](const char * stem, grotto::window which, unsigned bits) {
auto samples = probe(which, bits,
[](grotto::window w, unsigned b, std::int64_t raw) {
return grotto::eval_window(w, b, raw);
}, around_one(bits));
add_table(works, "window",
std::string("window_") + stem + "_p" + std::to_string(bits),
[which, bits](std::int64_t raw) {
return grotto::eval_window(which, bits, raw);
}, std::move(samples));
};
const auto add_principal = [&](const char * stem, grotto::principal which, unsigned bits) {
auto samples = probe(which, bits,
[](grotto::principal w, unsigned b, std::int64_t raw) {
return grotto::eval_principal(w, b, raw);
}, around_one(bits));
add_table(works, "principal",
std::string("principal_") + stem + "_p" + std::to_string(bits),
[which, bits](std::int64_t raw) {
return grotto::eval_principal(which, bits, raw);
}, std::move(samples));
};
const auto add_closed = [&](const char * stem, grotto::closed which, unsigned bits) {
auto samples = probe(which, bits,
[](grotto::closed w, unsigned b, std::int64_t raw) {
return grotto::eval_closed(w, b, raw);
}, around_one(bits));
add_table(works, "closed",
std::string("closed_") + stem + "_p" + std::to_string(bits),
[which, bits](std::int64_t raw) {
return grotto::eval_closed(which, bits, raw);
}, std::move(samples));
};
const auto add_reduced = [&](const char * stem, grotto::reduced which, unsigned bits) {
auto samples = probe(which, bits,
[](grotto::reduced w, unsigned b, std::int64_t raw) {
return grotto::eval_reduced(w, b, raw);
}, around_one(bits));
add_table(works, "reduced",
std::string("reduced_") + stem + "_p" + std::to_string(bits),
[which, bits](std::int64_t raw) {
return grotto::eval_reduced(which, bits, raw);
}, std::move(samples));
};
const struct { const char * stem; grotto::window which; } windows[] = {
{"gelu", grotto::window::gelu},
{"sigmoid", grotto::window::sigmoid},
{"tanh", grotto::window::tanh},
{"erf", grotto::window::erf},
{"erfc", grotto::window::erfc},
{"silu", grotto::window::silu},
{"mish", grotto::window::mish},
{"softplus", grotto::window::softplus},
{"probit", grotto::window::probit},
{"smoothstep", grotto::window::smoothstep},
{"asin", grotto::window::asin},
{"acos", grotto::window::acos},
{"hardelish", grotto::window::hardelish},
{"lecun_tanh", grotto::window::lecun_tanh},
};
for (const auto & spec : windows)
for (const unsigned bits : precisions)
add_window(spec.stem, spec.which, bits);
const struct { const char * stem; grotto::principal which; } principals[] = {
{"ln", grotto::principal::ln},
{"exp", grotto::principal::exp},
{"sin", grotto::principal::sin},
{"sqrt", grotto::principal::sqrt},
{"sinh", grotto::principal::sinh},
{"inv", grotto::principal::inv},
};
for (const auto & spec : principals)
for (const unsigned bits : precisions)
add_principal(spec.stem, spec.which, bits);
const struct { const char * stem; grotto::closed which; } closeds[] = {
{"atan", grotto::closed::atan},
{"cbrt", grotto::closed::cbrt},
{"sinc", grotto::closed::sinc},
{"softsign", grotto::closed::softsign},
{"logistic", grotto::closed::logistic},
};
for (const auto & spec : closeds)
for (const unsigned bits : precisions)
add_closed(spec.stem, spec.which, bits);
const struct { const char * stem; grotto::reduced which; } reduceds[] = {
{"ln", grotto::reduced::ln},
{"exp", grotto::reduced::exp},
{"sin", grotto::reduced::sin},
{"expm1", grotto::reduced::expm1},
{"log1p", grotto::reduced::log1p},
{"sqrt", grotto::reduced::sqrt},
};
for (const auto & spec : reduceds)
for (const unsigned bits : precisions)
add_reduced(spec.stem, spec.which, bits);
const auto add_exact = [&](const std::string & name, unsigned bits, auto fn) {
std::vector<std::int64_t> samples;
for (const std::int64_t raw : around_one(bits))
{
try
{
(void)fn(raw);
samples.push_back(raw);
}
catch (const std::exception &)
{
}
}
add_table(works, "exact", name, fn, std::move(samples));
};
for (const unsigned bits : {8u, 16u, 32u})
{
const auto tag = "_p" + std::to_string(bits);
add_exact("exact_dec_floor" + tag, bits, [bits](std::int64_t raw) {
return grotto::eval_dec_floor(raw, bits);
});
add_exact("exact_bit_width" + tag, bits, [bits](std::int64_t raw) {
return grotto::eval_bit_width<std::int64_t>(raw, bits);
});
add_exact("exact_dec_width" + tag, bits, [bits](std::int64_t raw) {
return grotto::eval_dec_width(raw, bits);
});
add_exact("exact_ilog256" + tag, bits, [bits](std::int64_t raw) {
return grotto::eval_ilog256<std::int64_t>(raw, bits);
});
}
add_horner_degree<1>(works);
add_horner_degree<3>(works);
using in_t = std::uint32_t;
const in_t center = 50;
const in_t eta = 7;
for (const std::size_t degree : std::initializer_list<std::size_t>{1, 4, 8, 16})
{
works.push_back(cell("keygen", "poly_keygen_d" + std::to_string(degree), 1,
[degree] {
auto built = grotto::make_offset_poly_keys(in_t{50}, degree);
return touch_word(built.wrap_share[0][0],
built.wrap_share.size() * sizeof(built.wrap_share[0]));
}));
using mat_t = decltype(grotto::make_offset_poly_keys(center, degree));
auto mat = std::make_shared<mat_t>(grotto::make_offset_poly_keys(center, degree));
for (const std::size_t pieces : std::initializer_list<std::size_t>{4, 8, 16})
{
if (degree == 16 && pieces == 16)
continue;
auto knots = std::make_shared<std::vector<in_t>>(knots_of<in_t>(pieces, 800));
auto rows = std::make_shared<std::vector<std::vector<std::uint64_t>>>(
poly_rows(pieces, degree));
const auto name = "poly_eval_d" + std::to_string(degree)
+ "_p" + std::to_string(pieces);
works.push_back(cell("poly", name, 1, [mat, knots, rows] {
const auto a = grotto::offset_poly_eval<0>(*mat, *knots, *rows, eta, nullptr);
const auto b = grotto::offset_poly_eval<1>(*mat, *knots, *rows, eta, nullptr);
return touch_word(a ^ b, 16);
}));
}
}
for (const std::size_t degree : std::initializer_list<std::size_t>{1, 4, 8, 16})
{
works.push_back(cell("keygen", "jet_keygen_d" + std::to_string(degree), 1,
[degree] {
auto built = grotto::make_offset_jet_keys(in_t{50}, degree);
return touch_word(built.wrap_share[0][0],
built.wrap_share.size() * sizeof(built.wrap_share[0]));
}));
using mat_t = decltype(grotto::make_offset_jet_keys(center, degree));
auto mat = std::make_shared<mat_t>(grotto::make_offset_jet_keys(center, degree));
for (const std::size_t pieces : std::initializer_list<std::size_t>{4, 16})
{
auto knots = std::make_shared<std::vector<in_t>>(knots_of<in_t>(pieces, 800));
auto col = std::make_shared<std::vector<std::uint64_t>>(coeff_col(degree));
const auto name = "jet_eval_d" + std::to_string(degree)
+ "_p" + std::to_string(pieces);
works.push_back(cell("jet", name, 1, [mat, knots, col] {
const auto a = grotto::offset_jet_eval<0>(*mat, *knots, *col, eta);
const auto b = grotto::offset_jet_eval<1>(*mat, *knots, *col, eta);
return touch_word(a ^ b, 16);
}));
}
}
for (const std::size_t degree : std::initializer_list<std::size_t>{1, 4, 8})
{
auto knots = std::make_shared<std::vector<in_t>>(knots_of<in_t>(8, 900));
auto col = std::make_shared<std::vector<std::uint64_t>>(coeff_col(degree));
using odd_t = decltype(grotto::make_offset_twist_keys(center, degree, std::uint64_t{3}));
using half_t = decltype(grotto::make_offset_twist_keys(center, degree, grotto::twist_half));
auto odd = std::make_shared<odd_t>(
grotto::make_offset_twist_keys(center, degree, std::uint64_t{3}));
auto half = std::make_shared<half_t>(
grotto::make_offset_twist_keys(center, degree, grotto::twist_half));
const auto dtag = std::to_string(degree);
works.push_back(cell("keygen", "twist_keygen_odd_d" + dtag, 1, [degree] {
auto built = grotto::make_offset_twist_keys(in_t{50}, degree, std::uint64_t{3});
return touch_word(built.wrap_share[0][0],
built.wrap_share.size() * sizeof(built.wrap_share[0]));
}));
works.push_back(cell("twist", "twist_eval_odd_d" + dtag + "_p8", 1,
[odd, knots, col] {
const auto a = grotto::offset_twist_eval<0>(*odd, *knots, *col, eta);
const auto b = grotto::offset_twist_eval<1>(*odd, *knots, *col, eta);
return touch_word(a ^ b, 16);
}));
works.push_back(cell("twist", "twist_eval_half_d" + dtag + "_p8", 1,
[half, knots, col] {
const auto a = grotto::offset_twist_eval<0>(*half, *knots, *col, eta);
const auto b = grotto::offset_twist_eval<1>(*half, *knots, *col, eta);
return touch_word(a ^ b, 16);
}));
}
{
const std::vector<std::uint64_t> fib_state{1, 0};
const std::vector<std::uint64_t> wide_state{1, 2, 3, 4};
using fib_t = decltype(grotto::make_offset_repr_keys(center, fib_state));
using wide_t = decltype(grotto::make_offset_repr_keys(center, wide_state));
auto fib = std::make_shared<fib_t>(grotto::make_offset_repr_keys(center, fib_state));
auto wide = std::make_shared<wide_t>(grotto::make_offset_repr_keys(center, wide_state));
auto fib_m = std::make_shared<std::vector<std::vector<std::uint64_t>>>(
grotto::offset_repr_fibonacci_matrix());
auto tri_m = std::make_shared<std::vector<std::vector<std::uint64_t>>>(
std::vector<std::vector<std::uint64_t>>{
{1, 1, 0, 0},
{0, 1, 1, 0},
{0, 0, 1, 1},
{0, 0, 0, 1},
});
works.push_back(cell("keygen", "repr_keygen_dim2", 1, [] {
auto built = grotto::make_offset_repr_keys(in_t{50}, std::vector<std::uint64_t>{1, 0});
return touch_word(built.wrap_share[0][0],
built.wrap_share.size() * sizeof(built.wrap_share[0]));
}));
works.push_back(cell("keygen", "repr_keygen_dim4", 1, [] {
auto built = grotto::make_offset_repr_keys(in_t{50},
std::vector<std::uint64_t>{1, 2, 3, 4});
return touch_word(built.wrap_share[0][0],
built.wrap_share.size() * sizeof(built.wrap_share[0]));
}));
for (const std::size_t pieces : std::initializer_list<std::size_t>{4, 8, 16})
{
auto knots = std::make_shared<std::vector<in_t>>(knots_of<in_t>(pieces, 700));
const auto tag = "_p" + std::to_string(pieces);
works.push_back(cell("repr", "repr_eval_fib" + tag, 1, [fib, fib_m, knots] {
const auto a = grotto::offset_repr_eval<0>(*fib, *fib_m, *knots, eta);
const auto b = grotto::offset_repr_eval<1>(*fib, *fib_m, *knots, eta);
return touch_vec(a) + touch_vec(b);
}));
works.push_back(cell("repr", "repr_eval_dim4" + tag, 1, [wide, tri_m, knots] {
const auto a = grotto::offset_repr_eval<0>(*wide, *tri_m, *knots, eta);
const auto b = grotto::offset_repr_eval<1>(*wide, *tri_m, *knots, eta);
return touch_vec(a) + touch_vec(b);
}));
}
works.push_back(cell("repr", "repr_crc32_jump", 64, [] {
std::uint32_t state = 0x12345678u;
for (unsigned i = 0; i < 64; ++i)
state = grotto::offset_repr_crc32_jump(state, 1ull << (i % 17));
return touch_word(state, 64u * 32u * sizeof(std::uint32_t));
}));
}
add_prefix_width<std::uint16_t>(works, "u16", 400);
add_prefix_width<std::uint32_t>(works, "u32", 100000);
return profile::run_works(opt, works);
}