// MIT License // // Copyright (c) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #ifndef ROCTHRUST_BENCHMARKS_BENCH_UTILS_CUSTOM_REPORTER_HPP_ #define ROCTHRUST_BENCHMARKS_BENCH_UTILS_CUSTOM_REPORTER_HPP_ // Utils #include "common/types.hpp" // Google Benchmark #include // STL #include #include #include #include #include #include #include #include #include #include #include #include namespace bench_utils { template bool IsType(const SrcType* src) { // Check if the src can be casted to the DstType return dynamic_cast(src) != nullptr; } static std::string FormatString(const char* msg, va_list args) { // we might need a second shot at this, so pre-emptivly make a copy va_list args_cp; va_copy(args_cp, args); std::size_t size = 256; char local_buff[256]; auto ret = vsnprintf(local_buff, size, msg, args_cp); if (ret <= 0) { return {}; } else if (static_cast(ret) < size) { return local_buff; } // we did not provide a long enough buffer on our first attempt. size = static_cast(ret) + 1; // + 1 for the null byte std::unique_ptr buff(new char[size]); ret = vsnprintf(buff.get(), size, msg, args); return buff.get(); } static std::string FormatString(const char* msg, ...) { va_list args; va_start(args, msg); auto tmp = FormatString(msg, args); va_end(args); return tmp; } std::string get_complexity(const benchmark::BigO& complexity) { switch (complexity) { case benchmark::oN: return "N"; case benchmark::oNSquared: return "N^2"; case benchmark::oNCubed: return "N^3"; case benchmark::oLogN: return "lgN"; case benchmark::oNLogN: return "NlgN"; case benchmark::o1: return "(1)"; default: return "f(N)"; } } template double calculate_bw_utils(const Run& result) { // Calculates bandwith utilization in % std::map* global_context = benchmark::internal::GetGlobalContext(); if (global_context != nullptr) { for (const auto& keyval : *global_context) { if (keyval.first == "hdp_peak_global_mem_bus_bandwidth") { const double global_mem_bw = result.counters.at("bytes_per_second").value; const double peak_global_bw = std::stod(keyval.second); const double bw_util = 100. * global_mem_bw / peak_global_bw; return bw_util; } } } return -1; } /// \brief Custom Google Benchmark reporter for formatting the benchmarks' report matching Thrust's. /// /// This reporter is a ConsoleReporter that outputs: /// - GPU Time: measured with events and registered as manual time. /// - CPU Time: measured by Google Benchmark. /// - Iterations: the number of kernel executions done on each benchmark run. /// - GlobalMem BW: number of GB read and written to global memory per second of execution. /// - BW util: percentage of the theoretical peak global memory bandwith utilised by the benchmark. /// - Elements/s: number of G elements (G = 10**6) processed by second of execution. /// - Labels: for extra labels. For instance, labels are used by the benchmarks generating random /// input values to report the seed used. /// /// Additionally, when the number of \p repetitions is greater than one, each benchmark run is /// repeated \p repetitions times to measure the stability of results. In this case, the mean, /// median, standard deviation (stddev) and coefficient of variation (cv) of the above-described /// metrics are also reported after all the \p repetitions have ben run. class CustomConsoleReporter : public benchmark::ConsoleReporter { private: enum LogColor { COLOR_DEFAULT, COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, COLOR_CYAN, COLOR_WHITE }; std::string get_log_color(LogColor&& color) { std::string s("unknown"); switch (color) { case COLOR_RED: return "\033[31m"; case COLOR_GREEN: return "\033[32m"; case COLOR_YELLOW: return "\033[33m"; case COLOR_BLUE: return "\033[34m"; case COLOR_CYAN: return "\033[36m"; case COLOR_WHITE: return "\033[37m"; default: // COLOR_DEFAULT return "\033[0m"; } } void PrintColoredString(std::ostream& os, std::string color, std::string str, ...) { os << color; va_list args; va_start(args, str); os << FormatString(str.data(), args); va_end(args); } static std::string FormatTime(double time) { // Assuming the time is at max 9.9999e+99 and we have 10 digits for the // number, we get 10-1(.)-1(e)-1(sign)-2(exponent) = 5 digits to print. if (time > 9999999999 /*max 10 digit number*/) { return FormatString("%1.4e", time); } return FormatString("%10.3f", time); } public: void PrintHeader(const Run& /*run*/) { // Assume run.counters (with elements processed and global mem reads/writes) // will not be empty std::string str = FormatString( "%-*s %13s %15s %12s %16s %12s %14s %14s", static_cast(name_field_width_), "Benchmark", "GPU Time", "CPU Time", "Iterations", "GlobalMem BW", "BW util", "GPU Noise", "Elements/s"); std::string line = std::string(str.length(), '-'); GetOutputStream() << line << "\n" << str << "\n" << line << "\n"; } void PrintRunData(const Run& result) { // Report benchmark name auto& sout = GetOutputStream(); auto color = get_log_color((result.report_big_o || result.report_rms) ? COLOR_BLUE : COLOR_GREEN); PrintColoredString(sout, color, "%-*s ", name_field_width_, result.benchmark_name().c_str()); if (benchmark::internal::SkippedWithError == result.skipped) { PrintColoredString(sout, get_log_color(COLOR_RED), "ERROR OCCURRED: \'%s\'", result.skip_message.c_str()); PrintColoredString(sout, get_log_color(COLOR_DEFAULT), "\n"); return; } else if (benchmark::internal::SkippedWithMessage == result.skipped) { PrintColoredString(sout, get_log_color(COLOR_WHITE), "SKIPPED: \'%s\'", result.skip_message.c_str()); PrintColoredString(sout, get_log_color(COLOR_DEFAULT), "\n"); return; } // Report GPU and CPU time const double gpu_time = result.GetAdjustedRealTime(); const double cpu_time = result.GetAdjustedCPUTime(); const std::string gpu_time_str = FormatTime(gpu_time); const std::string cpu_time_str = FormatTime(cpu_time); if (result.report_big_o) { std::string big_o = get_complexity(result.complexity); PrintColoredString( sout, get_log_color(COLOR_YELLOW), "%10.3f %-4s %10.3f %-4s ", gpu_time, big_o.c_str(), cpu_time, big_o.c_str()); } else if (result.report_rms) { PrintColoredString( sout, get_log_color(COLOR_YELLOW), "%10.3f %-4s %10.3f %-4s ", gpu_time * 100, "%", cpu_time * 100, "%"); } else if (result.run_type != Run::RT_Aggregate || result.aggregate_unit == benchmark::StatisticUnit::kTime) { const char* timeLabel = GetTimeUnitString(result.time_unit); PrintColoredString( sout, get_log_color(COLOR_YELLOW), "%s %-4s %s %-4s ", gpu_time_str.c_str(), timeLabel, cpu_time_str.c_str(), timeLabel); } else { assert(result.aggregate_unit == benchmark::StatisticUnit::kPercentage); PrintColoredString( sout, get_log_color(COLOR_YELLOW), "%10.3f %-4s %10.3f %-4s ", (100. * result.real_accumulated_time), "%", (100. * result.cpu_accumulated_time), "%"); } // Report iterations if (!result.report_big_o && !result.report_rms) { PrintColoredString(sout, get_log_color(COLOR_CYAN), "%10lld", result.iterations); } // Report counters std::string s; const char* unit = ""; for (auto& c : result.counters) { std::size_t cNameLen = std::max(std::string::size_type(10), c.first.length()); if (c.first == "items_per_second") { // Print elements processed in G/s if (result.run_type == Run::RT_Aggregate && result.aggregate_unit == benchmark::StatisticUnit::kPercentage) { s = FormatString("%.3f", 100. * c.second.value); unit = "%"; } else { s = FormatString("%.3f", c.second.value / 1e9); unit = "G"; } } else if (c.first == "bytes_per_second") { // Print GlobalMem BW in GB/s if (result.run_type == Run::RT_Aggregate && result.aggregate_unit == benchmark::StatisticUnit::kPercentage) { s = FormatString("%.3f", 100. * c.second.value); unit = "%"; } else { s = FormatString("%.3f", c.second.value / 1e9); if (c.second.flags & benchmark::Counter::kIsRate) { unit = "GB/s"; } } PrintColoredString(sout, get_log_color(COLOR_DEFAULT), " %*s%s", cNameLen - strlen(unit), s.c_str(), unit); // Print BW util s = FormatString("%.2f", c.second.value); const double bw_util = calculate_bw_utils(result); if (bw_util >= 0) { s = FormatString("%.2f", bw_util); unit = "%"; cNameLen = std::max(std::string::size_type(12), s.length()); } } else if (c.first == "gpu_noise") { if (result.run_type == Run::RT_Aggregate && result.aggregate_unit == benchmark::StatisticUnit::kPercentage) { s = FormatString("%.2f", 100. * c.second.value); } else { s = FormatString("%.2f", c.second.value); } unit = "%"; } else { if (result.run_type == Run::RT_Aggregate && result.aggregate_unit == benchmark::StatisticUnit::kPercentage) { s = FormatString("%.2f", 100. * c.second.value); unit = "%"; } else { s = FormatString("%.2f", c.second.value / 1000.); if (c.second.flags & benchmark::Counter::kIsRate) { unit = (c.second.flags & benchmark::Counter::kInvert) ? "s" : "/s"; } } } PrintColoredString(sout, get_log_color(COLOR_DEFAULT), " %*s%s", cNameLen - strlen(unit), s.c_str(), unit); } // Print labels, that is, GPU noise if (!result.report_label.empty()) { std::string s = FormatString("%.2f", 100. * std::stod(result.report_label)); const char* unit = "%"; PrintColoredString(sout, get_log_color(COLOR_DEFAULT), " %13s%s", s.c_str(), unit); } PrintColoredString(sout, get_log_color(COLOR_DEFAULT), "\n"); } // Called once for each group of benchmark runs, gives information about // cpu-time, gpu-time, elements processed, global memory bw and the % // the latter represents from the peak global bandwidth during the // benchmark run. If the group of runs contained more than two entries // then 'report' contains additional elements representing the mean, // standard deviation and coefficient of variation of those runs. // Additionally if this group of runs was the last in a family of // benchmarks, 'reports' contains additional entries representing the // asymptotic complexity and RMS of that benchmark family. void ReportRuns(const std::vector& reports) { for (const auto& run : reports) { // Print the header if none was printed yet bool print_header = !printed_header_; if (print_header) { printed_header_ = true; PrintHeader(run); } PrintRunData(run); } } }; class CustomJSONReporter : public benchmark::JSONReporter { private: bool first_report_ = true; std::string StrEscape(const std::string& s) { std::string tmp; tmp.reserve(s.size()); for (char c : s) { switch (c) { case '\b': tmp += "\\b"; break; case '\f': tmp += "\\f"; break; case '\n': tmp += "\\n"; break; case '\r': tmp += "\\r"; break; case '\t': tmp += "\\t"; break; case '\\': tmp += "\\\\"; break; case '"': tmp += "\\\""; break; default: tmp += c; break; } } return tmp; } std::string FormatKV(std::string const& key, std::string const& value) { return FormatString("\"%s\": \"%s\"", StrEscape(key).c_str(), StrEscape(value).c_str()); } std::string FormatKV(std::string const& key, const char* value) { return FormatString("\"%s\": \"%s\"", StrEscape(key).c_str(), StrEscape(value).c_str()); } std::string FormatKV(std::string const& key, bool value) { return FormatString("\"%s\": %s", StrEscape(key).c_str(), value ? "true" : "false"); } std::string FormatKV(std::string const& key, int64_t value) { std::stringstream ss; ss << '"' << StrEscape(key) << "\": " << value; return ss.str(); } std::string FormatKV(std::string const& key, double value) { std::stringstream ss; ss << '"' << StrEscape(key) << "\": "; if (std::isnan(value)) { ss << (value < 0 ? "-" : "") << "NaN"; } else if (std::isinf(value)) { ss << (value < 0 ? "-" : "") << "Infinity"; } else { const auto max_digits10 = std::numeric_limits::max_digits10; const auto max_fractional_digits10 = max_digits10 - 1; ss << std::scientific << std::setprecision(max_fractional_digits10) << value; } return ss.str(); } public: void ReportRuns(std::vector const& reports) { if (reports.empty()) { return; } std::string indent(4, ' '); std::ostream& out = GetOutputStream(); if (!first_report_) { out << ",\n"; } first_report_ = false; for (auto it = reports.begin(); it != reports.end(); ++it) { out << indent << "{\n"; PrintRunData(*it); out << indent << '}'; auto it_cp = it; if (++it_cp != reports.end()) { out << ",\n"; } } } void PrintRunData(Run const& run) { std::string indent(6, ' '); std::ostream& out = GetOutputStream(); auto output_format = [this, &out, &indent](const std::string& label, auto val, bool start_endl = true) { if (start_endl) { out << ",\n"; } out << indent << FormatKV(label, val); }; output_format("name", run.benchmark_name(), false); output_format("family_index", run.family_index); output_format("per_family_instance_index", run.per_family_instance_index); output_format("run_name", run.run_name.str()); output_format("run_type", [&run]() -> const char* { switch (run.run_type) { case BenchmarkReporter::Run::RT_Iteration: return "iteration"; case BenchmarkReporter::Run::RT_Aggregate: return "aggregate"; } BENCHMARK_UNREACHABLE(); }()); output_format("repetitions", run.repetitions); if (run.run_type != BenchmarkReporter::Run::RT_Aggregate) { output_format("repetition_index", run.repetition_index); } output_format("threads", run.threads); if (run.run_type == BenchmarkReporter::Run::RT_Aggregate) { output_format("aggregate_name", run.aggregate_name); output_format("aggregate_unit", [&run]() -> const char* { switch (run.aggregate_unit) { case benchmark::StatisticUnit::kTime: return "time"; case benchmark::StatisticUnit::kPercentage: return "percentage"; } BENCHMARK_UNREACHABLE(); }()); } if (benchmark::internal::SkippedWithError == run.skipped) { output_format("error_occurred", true); output_format("error_message", run.skip_message); } else if (benchmark::internal::SkippedWithMessage == run.skipped) { output_format("skipped", true); output_format("skip_message", run.skip_message); } if (!run.report_big_o && !run.report_rms) { output_format("iterations", run.iterations); if (run.run_type != Run::RT_Aggregate || run.aggregate_unit == benchmark::StatisticUnit::kTime) { output_format("gpu_time", run.GetAdjustedRealTime()); output_format("cpu_time", run.GetAdjustedCPUTime()); } else { assert(run.aggregate_unit == benchmark::StatisticUnit::kPercentage); output_format("gpu_time", run.real_accumulated_time); output_format("cpu_time", run.cpu_accumulated_time); } output_format("time_unit", GetTimeUnitString(run.time_unit)); } else if (run.report_big_o) { output_format("cpu_coefficient", run.GetAdjustedCPUTime()); output_format("gpu_coefficient", run.GetAdjustedRealTime()); output_format("big_o", get_complexity(run.complexity)); output_format("time_unit", GetTimeUnitString(run.time_unit)); } else if (run.report_rms) { output_format("rms", run.GetAdjustedCPUTime()); } for (auto& c : run.counters) { if (c.first == "items_per_second") { // Report same name as console reporter output_format("elements_per_second", c.second); } else if (c.first == "bytes_per_second") { // Report same name as console reporter output_format("global_mem_bw", c.second); const double util_bw = calculate_bw_utils(run); if (util_bw >= 0) { output_format("util_bw", util_bw); } } else { output_format(c.first, c.second); } } const benchmark::MemoryManager::Result memory_result = run.memory_result; output_format("allocs_per_iter", run.allocs_per_iter); output_format("max_bytes_used", memory_result.max_bytes_used); if (memory_result.total_allocated_bytes != benchmark::MemoryManager::TombstoneValue) { output_format("total_allocated_bytes", memory_result.total_allocated_bytes); } if (memory_result.net_heap_growth != benchmark::MemoryManager::TombstoneValue) { output_format("net_heap_growth", memory_result.net_heap_growth); } if (!run.report_label.empty()) { output_format("label", run.report_label); } out << '\n'; } }; BENCHMARK_DISABLE_DEPRECATED_WARNING class CustomCSVReporter : public benchmark::CSVReporter { private: bool printed_header_ = false; std::vector elements = { "name", "iterations", "gpu_time", "cpu_time", "time_unit", "global_mem_bw", "util_bw", "elements_per_second", "gpu_noise", "label", "error_occurred", "error_message"}; std::string CsvEscape(const std::string& s) { std::string tmp; tmp.reserve(s.size() + 2); for (char c : s) { switch (c) { case '"': tmp += "\"\""; break; default: tmp += c; break; } } return '"' + tmp + '"'; } public: void PrintHeader(const Run& /*run*/) { std::string str = ""; bool first = true; for (auto element : elements) { if (first) { first = false; } else { str += ","; } str += element; } GetOutputStream() << str << "\n"; } void PrintRunData(const Run& result) { // Report benchmark name auto& sout = GetOutputStream(); if (result.skipped) { sout << std::string(elements.size() - 3, ','); sout << std::boolalpha << (benchmark::internal::SkippedWithError == result.skipped) << ","; sout << CsvEscape(result.skip_message) << "\n"; return; } sout << CsvEscape(result.benchmark_name()) << ","; if (!result.report_big_o && !result.report_rms) { sout << result.iterations; } sout << ","; sout << result.GetAdjustedRealTime() << ","; sout << result.GetAdjustedCPUTime() << ","; if (result.report_big_o) { sout << get_complexity(result.complexity); } else if (!result.report_rms) { sout << benchmark::GetTimeUnitString(result.time_unit); } sout << ","; if (result.counters.find("bytes_per_second") != result.counters.end()) { sout << result.counters.at("bytes_per_second"); } sout << ","; if (result.counters.find("bytes_per_second") != result.counters.end()) { const double bw_util = calculate_bw_utils(result); if (bw_util >= 0) { sout << bw_util; } } sout << ","; if (result.counters.find("items_per_second") != result.counters.end()) { sout << result.counters.at("items_per_second"); } sout << ","; if (result.counters.find("gpu_noise") != result.counters.end()) { sout << result.counters.at("gpu_noise"); } sout << ","; if (!result.report_label.empty()) { sout << CsvEscape(result.report_label); } sout << ",,\n"; } void ReportRuns(const std::vector& reports) { for (const auto& run : reports) { // Print the header if none was printed yet bool print_header = !printed_header_; if (print_header) { printed_header_ = true; PrintHeader(run); } PrintRunData(run); } } }; benchmark::BenchmarkReporter* ChooseCustomReporter() { // benchmark::BenchmarkReporter is polymorphic as it has a virtual // function which allows us to use dynamic_cast to detect the derived type. using PtrType = benchmark::BenchmarkReporter*; PtrType default_display_reporter = benchmark::CreateDefaultDisplayReporter(); if (IsType(default_display_reporter)) { return PtrType(new CustomCSVReporter); } else if (IsType(default_display_reporter)) { return PtrType(new CustomJSONReporter); } else if (IsType(default_display_reporter)) { return PtrType(new CustomConsoleReporter); } return nullptr; } BENCHMARK_RESTORE_DEPRECATED_WARNING } // namespace bench_utils #endif // ROCTHRUST_BENCHMARKS_BENCH_UTILS_CUSTOM_REPORTER_HPP_