| // Copyright 2020 The IREE Authors | |
| // | |
| // Licensed 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 | |
| //===----------------------------------------------------------------------===// | |
| // iree-benchmark-module: benchmarks public functions in an IREE VM module | |
| //===----------------------------------------------------------------------===// | |
| // | |
| // This runs exported functions using flags specified on the command line. | |
| // Each function is measured independently and the numbers reported will be for | |
| // the full end-to-end CPU and wall times. | |
| // | |
| // From an ML perspective this is an integration benchmark for measuring total | |
| // user-visible latency of model entry points. It is *not* a microbenchmarking | |
| // tool for individual device-side dispatch functions (aka ops aka kernels). | |
| // --dispatch_statistics adds a breakdown by dispatch function, collected with a | |
| // lightweight HAL profiling session in a separate untimed pass that runs the | |
| // program again; it shows where time goes rather than timing a kernel. | |
| // If interested in the precise time of a particular dispatch then tracy, | |
| // executable_library_benchmark, and platform/vendor tooling (nsight, perf, etc) | |
| // are to be used instead and attaching them to this tool is often useful in | |
| // order to get a large sample set. | |
| // | |
| // By default all functions taking no inputs will be benchmarked. If a function | |
| // takes inputs then the user will need to specify them using --input= | |
| // flags. Depending on the input program the -iree-flow-export-benchmark-funcs | |
| // flag can be passed to the compiler to attempt to wrap each function with | |
| // dummy inputs however this will fail in programs with dynamically shaped | |
| // inputs. The workaround for avoiding the need for flags is to provide the | |
| // input program in a form with no inputs from the start. | |
| // | |
| // It's important to remember that IREE is not a BLAS library and is meant to | |
| // run entire programs. It's not generally appropriate to benchmark a model with | |
| // a single matmul, for example, as that's just treating IREE as a BLAS library. | |
| // Note also that user-level ops in a frontend environment don't map to the | |
| // dispatches that IREE executes: IREE is a compiler like any other and does not | |
| // guarantee a source line of code translates into an atomically divisible and | |
| // independently measurable execution command. In other words don't expect to be | |
| // able to benchmark the cost of a broadcasting elementwise tf.add op within a | |
| // model: by the time we are running the program that's fused itself into a | |
| // single machine instruction operating as part of some other ops. | |
| // | |
| // For coarse dispatch testing and triaging it can still be useful to remove | |
| // some of the overheads introduced by whole-program execution and the compiler | |
| // flag --iree-hal-benchmark-dispatch-repeat-count=N is provided to enable | |
| // batching. Whatever N is chosen must then be passed to this tool via | |
| // --batch_size=N so that the benchmark reporting properly reflects the | |
| // batching. As an example --iree-hal-benchmark-dispatch-repeat-count=32 + | |
| // --batch_size=32 will reduce the overheads by 32x. Think of this as a way to | |
| // control the p value in Amdahl's law representing the amount of time spent in | |
| // dispatches relative to the rest of the program. This isn't representative of | |
| // how the full program will run, though, and YMMV. Always verify timings with | |
| // an appropriate device-specific tool before trusting the more generic and | |
| // higher-level numbers from this tool. | |
| #include <algorithm> | |
| #include <cinttypes> | |
| #include <cstdio> | |
| #include <cstdlib> | |
| #include <cstring> | |
| #include <map> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "benchmark/benchmark.h" | |
| #include "iree/base/api.h" | |
| #include "iree/base/tooling/flags.h" | |
| #include "iree/hal/api.h" | |
| #include "iree/hal/replay/recorder.h" | |
| #include "iree/hal/utils/statistics_sink.h" | |
| #include "iree/modules/hal/types.h" | |
| #include "iree/tooling/context_util.h" | |
| #include "iree/tooling/device_util.h" | |
| #include "iree/tooling/function_io.h" | |
| #include "iree/tooling/function_util.h" | |
| #include "iree/tooling/process_results.h" | |
| #include "iree/vm/api.h" | |
| // Google Benchmark creates its own file reporter only when the caller passes | |
| // none, so wrapping it requires the output flags that Initialize parsed. They | |
| // are defined by the library but not declared in its public header. | |
| namespace benchmark { | |
| BENCHMARK_EXPORT extern std::string FLAGS_benchmark_out; | |
| BENCHMARK_EXPORT extern std::string FLAGS_benchmark_out_format; | |
| BENCHMARK_EXPORT extern bool FLAGS_benchmark_counters_tabular; | |
| } // namespace benchmark | |
| constexpr char kNanosecondsUnitString[] = "ns"; | |
| constexpr char kMicrosecondsUnitString[] = "us"; | |
| constexpr char kMillisecondsUnitString[] = "ms"; | |
| // TODO(hanchung): Extract the batch size using | |
| // iree_vm_function_lookup_attr_by_name. | |
| IREE_FLAG(int32_t, batch_size, 1, | |
| "Number of invocations per iteration, which for dispatch benchmarks " | |
| "must match the --iree-hal-benchmark-dispatch-repeat-count value " | |
| "used during compilation."); | |
| IREE_FLAG(int32_t, batch_concurrency, 1, | |
| "Number of invocations within a batch that should run concurrently."); | |
| IREE_FLAG(bool, agents_md, false, | |
| "Prints AGENTS.md guidance for iree-benchmark-module replay capture " | |
| "and exits."); | |
| IREE_FLAG(string, function, "", | |
| "Name of a function contained in the module specified by --module= " | |
| "to run. If this is not set, all the exported functions will be " | |
| "benchmarked and they are expected to not have input arguments."); | |
| IREE_FLAG(bool, print_statistics, false, | |
| "Prints runtime statistics to stderr on exit."); | |
| IREE_FLAG( | |
| bool, dispatch_statistics, false, | |
| "Adds a row per dispatch function under each benchmark result with the\n" | |
| "time spent in that function per benchmark iteration. The rows come from\n" | |
| "a separate untimed pass that reruns the benchmark with a lightweight HAL\n" | |
| "profiling session active after each measured repetition. Dispatch rows\n" | |
| "average all profiled repetitions. Requires a backend whose lightweight\n" | |
| "profiling attributes dispatches in existing command buffers; elsewhere\n" | |
| "the session fails to start or the rows are missing with a warning.\n" | |
| "Cannot combine with --device_profiling_mode, --print_device_statistics,\n" | |
| "--device_capture_tool, or --device_replay_output."); | |
| IREE_FLAG_LIST( | |
| string, input, | |
| "An input value or buffer of the format:\n" | |
| " [shape]xtype=[value]\n" | |
| " --input=\"2x2xi32=1 2 3 4\"\n" | |
| "Optionally, brackets may be used to separate the element values:\n" | |
| " --input=\"2x2xi32=[[1 2][3 4]]\"\n" | |
| "Raw binary files can be read to provide buffer contents:\n" | |
| " --input=2x2xi32=@some/file.bin\n" | |
| "numpy npy files (from numpy.save) can be read to provide 1+ values:\n" | |
| " --input=@some.npy\n" | |
| "Each occurrence of the flag indicates an input in the order they were\n" | |
| "specified on the command line."); | |
| static iree_status_t parse_time_unit(iree_string_view_t flag_name, | |
| void* storage, iree_string_view_t value) { | |
| auto* unit = (std::pair<bool, benchmark::TimeUnit>*)storage; | |
| auto unit_string = std::string(value.data, value.size); | |
| if (unit_string == kMillisecondsUnitString) { | |
| *unit = {true, benchmark::kMillisecond}; | |
| return iree_ok_status(); | |
| } else if (unit_string == kMicrosecondsUnitString) { | |
| *unit = {true, benchmark::kMicrosecond}; | |
| return iree_ok_status(); | |
| } else if (unit_string == kNanosecondsUnitString) { | |
| *unit = {true, benchmark::kNanosecond}; | |
| return iree_ok_status(); | |
| } | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "unsupported time unit"); | |
| } | |
| static void print_time_unit(iree_string_view_t flag_name, void* storage, | |
| FILE* file) { | |
| auto* unit = (std::pair<bool, benchmark::TimeUnit>*)storage; | |
| if (!unit->first) { | |
| return; | |
| } | |
| std::string unit_string; | |
| switch (unit->second) { | |
| case benchmark::kMillisecond: | |
| unit_string = kMillisecondsUnitString; | |
| break; | |
| case benchmark::kMicrosecond: | |
| unit_string = kMicrosecondsUnitString; | |
| break; | |
| case benchmark::kNanosecond: | |
| unit_string = kNanosecondsUnitString; | |
| break; | |
| default: | |
| assert(false && "Unexpected time unit."); | |
| } | |
| fprintf(file, "--%.*s=\"%s\"\n", (int)flag_name.size, flag_name.data, | |
| unit_string.c_str()); | |
| } | |
| // Time unit to be printed. If the first field is false, each place will use its | |
| // default time unit. | |
| static std::pair<bool, benchmark::TimeUnit> FLAG_time_unit = { | |
| false, benchmark::kNanosecond}; | |
| IREE_FLAG_CALLBACK( | |
| parse_time_unit, print_time_unit, &FLAG_time_unit, time_unit, | |
| "The time unit to be printed in the results. Can be 'ms', 'us', or 'ns'."); | |
| IREE_FLAG( | |
| bool, enable_output_processing, false, | |
| "Enable keeping outputs of last benchmark iteration and processing " | |
| "those. This needs to be enabled for --output* options to be effective."); | |
| namespace iree { | |
| namespace { | |
| static const char kIreeBenchmarkModuleUsage[] = | |
| "Benchmarks exported functions from a compiled IREE module.\n" | |
| "\n" | |
| "Replay capture wraps the resolved HAL device group after normal " | |
| "--device=\n" | |
| "selection. Use --device_replay_output=path.ireereplay to record the HAL\n" | |
| "work issued by the benchmark. The recorder is closed after benchmark\n" | |
| "execution so the output file is complete when the tool exits.\n" | |
| "\n" | |
| "Replay capture flags:\n" | |
| " --device_replay_output=path.ireereplay\n" | |
| " Writes a HAL replay stream.\n" | |
| " --device_replay_file_policy=reference|capture-ranges|capture-all|fail\n" | |
| " Controls imported fd-backed HAL files such as parameter archives.\n" | |
| " --device_replay_file_validation=identity|digest\n" | |
| " Validation for referenced fd-backed files.\n" | |
| " --agents_md\n" | |
| " Prints AGENTS.md guidance specific to iree-benchmark-module " | |
| "capture.\n" | |
| " Use `iree-run-replay --agents_md` for the full replay tool " | |
| "playbook.\n"; | |
| static void PrintBenchmarkModuleAgentMarkdown(FILE* file) { | |
| fputs( | |
| "# iree-benchmark-module Replay Capture\n" | |
| "\n" | |
| "`iree-benchmark-module` can capture the HAL work issued by benchmark\n" | |
| "iterations with `--device_replay_output=/path/to/model.ireereplay`.\n" | |
| "Capture flags compose with normal Google Benchmark controls.\n" | |
| "\n" | |
| "Synchronous and dispatch benchmarks record replay `execute` scopes " | |
| "around\n" | |
| "the VM invoke/list reset body. Asynchronous benchmarks record the " | |
| "scope\n" | |
| "around the resumed timing interval and keep batch setup and cleanup " | |
| "outside\n" | |
| "the selected scope. Use `iree-benchmark-replay --replay_scope=execute` " | |
| "to\n" | |
| "time the same region later while replay still executes the full " | |
| "captured\n" | |
| "stream.\n" | |
| "\n" | |
| "Use `--device_replay_file_policy=reference` for large stable parameter\n" | |
| "archives and `--device_replay_file_validation=identity` unless the " | |
| "files\n" | |
| "will move across filesystems and need digest validation.\n" | |
| "\n" | |
| "For replay execution, executable substitution, file remapping, dump " | |
| "JSONL,\n" | |
| "and the shared replay failure contract, pipe `iree-run-replay " | |
| "--agents_md`\n" | |
| "into your AGENTS.md.\n", | |
| file); | |
| } | |
| // How a benchmarked function is called, which decides what one measured batch | |
| // of --batch_size logical iterations executes. | |
| enum class InvocationKind { | |
| // A synchronous function. One VM invocation per batch; any repetition is | |
| // compiled into the function (--iree-hal-benchmark-dispatch-repeat-count). | |
| kSync, | |
| // A function with the coarse-fences ABI. A batch is --batch_size VM | |
| // invocations spread over --batch_concurrency fence timelines, followed by | |
| // one wait for all of them. | |
| kAsync, | |
| // A generated dispatch benchmark (iree.benchmark = "dispatch"). One VM | |
| // invocation per batch that takes the batch size as its repeat count. | |
| kDispatch, | |
| }; | |
| // Profiled time of one dispatch function, summed over all profiled iterations. | |
| struct DispatchRow { | |
| // Number of dispatches of the function. | |
| uint64_t calls = 0; | |
| // Total device time of those dispatches. | |
| uint64_t duration_ns = 0; | |
| // Worker time summed over all tiles of those dispatches, reported as CPU | |
| // time. Zero on backends that do not report tile totals. | |
| uint64_t tile_duration_ns = 0; | |
| }; | |
| // Per-dispatch results accumulated over all profiled repetitions. | |
| struct DispatchProfile { | |
| // Rows keyed by dispatch function name, or by executable id and function | |
| // ordinal when the backend provides no name. | |
| std::map<std::string, DispatchRow> rows; | |
| // Logical iterations of all completed profiling passes. Reported rows are | |
| // per iteration, so the totals are divided by this. | |
| int64_t iterations = 0; | |
| // Records the statistics sink dropped; nonzero makes the rows incomplete. | |
| uint64_t dropped_records = 0; | |
| // Dispatch rows skipped because they had no timing or their timestamps | |
| // could not be scaled to nanoseconds. | |
| uint64_t unscaled_rows = 0; | |
| }; | |
| // A benchmarked function with the ABI and iteration accounting resolved during | |
| // discovery. Measurement and profiling run the same workload. | |
| struct Workload { | |
| // Benchmark name, "BM_" followed by the function name. | |
| std::string name; | |
| // The exported VM function to invoke. | |
| iree_vm_function_t function = {}; | |
| // Calling convention of the results, used to print or check outputs. | |
| std::string results_cconv; | |
| InvocationKind kind = InvocationKind::kSync; | |
| // Logical iterations per measured batch: --batch_size rounded up to a | |
| // multiple of |concurrency|. | |
| int32_t batch_size = 1; | |
| // Number of fence timelines that async invocations run on; 1 otherwise. | |
| int32_t concurrency = 1; | |
| // Time unit of the reported rows. | |
| benchmark::TimeUnit unit = benchmark::kMillisecond; | |
| // Arguments shared by every invocation. Async invocations clone them and | |
| // append their wait and signal fences; dispatch benchmarks take the repeat | |
| // count. | |
| vm::ref<iree_vm_list_t> inputs; | |
| // Outputs of the last measured invocation, retained only with | |
| // --enable_output_processing. NULL if the function did not run. | |
| vm::ref<iree_vm_list_t> outputs; | |
| // Dispatch rows collected by --dispatch_statistics. | |
| DispatchProfile profile; | |
| }; | |
| // Arguments and results of one VM call within a batch. | |
| struct Invocation { | |
| vm::ref<iree_vm_list_t> inputs; | |
| vm::ref<iree_vm_list_t> outputs; | |
| }; | |
| // Owns one batch's arguments, outputs and synchronization. Only the async ABI | |
| // requires host-side batching; dispatch wrappers repeat inside the VM call. | |
| struct Batch { | |
| // One entry per VM call in the batch: |batch_size| for async workloads and | |
| // one otherwise. | |
| std::vector<Invocation> invocations; | |
| // Signaled when the last invocation on every timeline has completed. NULL | |
| // unless the workload is async. | |
| vm::ref<iree_hal_fence_t> completion; | |
| iree_status_t Prepare(const Workload& workload, iree_hal_device_t* device) { | |
| iree_allocator_t allocator = iree_allocator_system(); | |
| const bool async = workload.kind == InvocationKind::kAsync; | |
| const int count = async ? workload.batch_size : 1; | |
| invocations.resize(count); | |
| std::vector<vm::ref<iree_hal_semaphore_t>> timelines; | |
| std::vector<vm::ref<iree_hal_fence_t>> previous; | |
| if (async) { | |
| timelines.resize(workload.concurrency); | |
| previous.resize(workload.concurrency); | |
| IREE_RETURN_IF_ERROR( | |
| iree_hal_fence_create(workload.concurrency, allocator, &completion)); | |
| for (auto& timeline : timelines) { | |
| IREE_RETURN_IF_ERROR(iree_hal_semaphore_create( | |
| device, IREE_HAL_QUEUE_AFFINITY_ANY, 0, | |
| IREE_HAL_SEMAPHORE_FLAG_DEFAULT, &timeline)); | |
| IREE_RETURN_IF_ERROR(iree_hal_fence_insert( | |
| completion.get(), timeline.get(), count / workload.concurrency)); | |
| } | |
| } | |
| for (int i = 0; i < count; ++i) { | |
| auto& invocation = invocations[i]; | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_create(iree_vm_make_undefined_type_def(), 16, allocator, | |
| &invocation.outputs)); | |
| if (!async) { | |
| invocation.inputs = vm::retain_ref(workload.inputs.get()); | |
| continue; | |
| } | |
| const int track = i % workload.concurrency; | |
| vm::ref<iree_hal_fence_t> signal; | |
| IREE_RETURN_IF_ERROR(iree_hal_fence_create_at( | |
| timelines[track].get(), i / workload.concurrency + 1, allocator, | |
| &signal)); | |
| IREE_RETURN_IF_ERROR(iree_vm_list_clone(workload.inputs.get(), allocator, | |
| &invocation.inputs)); | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_push_ref_move(invocation.inputs.get(), previous[track])); | |
| previous[track] = vm::retain_ref(signal); | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_push_ref_move(invocation.inputs.get(), signal)); | |
| } | |
| return iree_ok_status(); | |
| } | |
| iree_status_t Execute(const Workload& workload, iree_vm_context_t* context) { | |
| for (auto& invocation : invocations) { | |
| // Preserve the measured lifetime of outputs: ordinary functions release | |
| // the preceding results before invoking; dispatch wrappers clear after. | |
| if (workload.kind == InvocationKind::kSync) { | |
| IREE_RETURN_IF_ERROR(iree_vm_list_resize(invocation.outputs.get(), 0)); | |
| } | |
| IREE_RETURN_IF_ERROR(iree_vm_invoke( | |
| context, workload.function, IREE_VM_INVOCATION_FLAG_NONE, nullptr, | |
| invocation.inputs.get(), invocation.outputs.get(), | |
| iree_allocator_system())); | |
| if (workload.kind == InvocationKind::kDispatch) { | |
| IREE_RETURN_IF_ERROR(iree_vm_list_resize(invocation.outputs.get(), 0)); | |
| } | |
| } | |
| return completion | |
| ? iree_hal_fence_wait(completion.get(), iree_infinite_timeout(), | |
| IREE_ASYNC_WAIT_FLAG_NONE) | |
| : iree_ok_status(); | |
| } | |
| }; | |
| // Runs a lightweight HAL statistics session on every device during Google | |
| // Benchmark's untimed profiling pass, which reruns the registered benchmark | |
| // after each measured repetition, and adds the per-dispatch rows to the | |
| // workload that pass runs. | |
| class DispatchProfiler final : public benchmark::ProfilerManager { | |
| public: | |
| explicit DispatchProfiler(iree_hal_device_list_t* devices) | |
| : devices_(devices) {} | |
| ~DispatchProfiler() { | |
| iree_status_ignore(End(/*workload=*/nullptr)); | |
| iree_status_ignore(status_); | |
| } | |
| // Starts and ends a session so unsupported backends fail before measuring. | |
| // Attribution also depends on backend metadata; an empty result is diagnosed | |
| // after execution. | |
| iree_status_t Probe() { | |
| IREE_RETURN_IF_ERROR(Begin()); | |
| return End(/*workload=*/nullptr); | |
| } | |
| // Selects the workload and state of the next benchmark function run. | |
| void Attach(Workload* workload, benchmark::State* state) { | |
| workload_ = workload; | |
| state_ = state; | |
| in_pass_ = false; | |
| } | |
| void Detach() { Attach(nullptr, nullptr); } | |
| // True from the start of a profiling pass until the next Attach. | |
| bool InPass() const { return in_pass_; } | |
| // The session of the running profiling pass, or NULL. | |
| iree_hal_profiling_session_t* Session() const { return session_; } | |
| bool Failed() const { return !iree_status_is_ok(status_); } | |
| // Returns and clears the failures of all profiling passes. | |
| iree_status_t ConsumeStatus() { | |
| return std::exchange(status_, iree_ok_status()); | |
| } | |
| void AfterSetupStart() override { | |
| in_pass_ = true; | |
| Check(Begin()); | |
| } | |
| void BeforeTeardownStop() override { | |
| // A skipped pass may have stopped early, so its rows are dropped. | |
| const bool completed = !state_->skipped(); | |
| if (Check(End(completed ? workload_ : nullptr)) && completed) { | |
| workload_->profile.iterations += state_->iterations(); | |
| } | |
| } | |
| private: | |
| bool Check(iree_status_t status) { | |
| if (iree_status_is_ok(status)) return true; | |
| if (state_) { | |
| state_->SkipWithError(iree::Status(iree_status_clone(status)).ToString()); | |
| } | |
| status_ = iree_status_join(status_, status); | |
| return false; | |
| } | |
| iree_status_t Begin() { | |
| IREE_RETURN_IF_ERROR(iree_hal_profile_statistics_sink_create( | |
| iree_allocator_system(), &sink_)); | |
| iree_hal_device_profiling_options_t options = {}; | |
| options.flags = IREE_HAL_DEVICE_PROFILING_FLAG_LIGHTWEIGHT_STATISTICS; | |
| options.sink = iree_hal_profile_statistics_sink_base(sink_); | |
| return iree_hal_profiling_session_begin( | |
| devices_->count, devices_->devices, &options, | |
| /*external_options=*/nullptr, /*flush_interval_ms=*/0, | |
| iree_allocator_system(), &session_); | |
| } | |
| // Ends the session and adds its rows to |workload| unless it is NULL. | |
| iree_status_t End(Workload* workload) { | |
| iree_status_t status = | |
| iree_hal_profiling_session_end(std::exchange(session_, nullptr)); | |
| if (!sink_) return status; | |
| if (workload && iree_status_is_ok(status)) { | |
| workload->profile.dropped_records += | |
| iree_hal_profile_statistics_sink_dropped_record_count(sink_); | |
| Collector collector = {sink_, &workload->profile}; | |
| iree_hal_profile_statistics_row_callback_t callback = {CollectRow, | |
| &collector}; | |
| status = iree_hal_profile_statistics_sink_for_each_row(sink_, callback); | |
| } | |
| iree_hal_profile_statistics_sink_release(std::exchange(sink_, nullptr)); | |
| return status; | |
| } | |
| // State passed to CollectRow while iterating the rows of a sink. | |
| struct Collector { | |
| // Sink being iterated, which scales timestamps and resolves names. | |
| iree_hal_profile_statistics_sink_t* sink; | |
| // Profile that receives the rows. | |
| DispatchProfile* profile; | |
| }; | |
| static iree_status_t CollectRow( | |
| void* data, const iree_hal_profile_statistics_row_t* row) { | |
| Collector& collector = *static_cast<Collector*>(data); | |
| if (row->row_type != | |
| IREE_HAL_PROFILE_STATISTICS_ROW_TYPE_DISPATCH_FUNCTION && | |
| row->row_type != | |
| IREE_HAL_PROFILE_STATISTICS_ROW_TYPE_HOST_EXECUTION_FUNCTION) { | |
| return iree_ok_status(); | |
| } | |
| uint64_t duration = 0; | |
| if (!iree_all_bits_set(row->flags, | |
| IREE_HAL_PROFILE_STATISTICS_ROW_FLAG_TIMING) || | |
| !iree_hal_profile_statistics_sink_scale_duration_to_ns( | |
| collector.sink, row, row->total_duration, &duration)) { | |
| ++collector.profile->unscaled_rows; | |
| return iree_ok_status(); | |
| } | |
| iree_string_view_t name; | |
| std::string key; | |
| if (iree_hal_profile_statistics_sink_find_function_name( | |
| collector.sink, row->executable_id, row->function_ordinal, &name)) { | |
| key.assign(name.data, name.size); | |
| } else { | |
| key = "executable_" + std::to_string(row->executable_id) + "_function_" + | |
| std::to_string(row->function_ordinal); | |
| } | |
| DispatchRow& result = collector.profile->rows[key]; | |
| result.calls += row->sample_count; | |
| result.duration_ns += duration; | |
| if (iree_all_bits_set(row->flags, | |
| IREE_HAL_PROFILE_STATISTICS_ROW_FLAG_TILE_TOTALS)) { | |
| result.tile_duration_ns += row->tile_duration_sum_ns; | |
| } | |
| return iree_ok_status(); | |
| } | |
| // Devices profiled in every pass. Unowned. | |
| iree_hal_device_list_t* devices_ = nullptr; | |
| // Session of the running pass, or NULL. | |
| iree_hal_profiling_session_t* session_ = nullptr; | |
| // Collects the records of |session_|; replaced for every pass. | |
| iree_hal_profile_statistics_sink_t* sink_ = nullptr; | |
| // Workload and state of the running benchmark function, set by Attach. | |
| Workload* workload_ = nullptr; | |
| benchmark::State* state_ = nullptr; | |
| bool in_pass_ = false; | |
| // Failures of all passes since the last ConsumeStatus. | |
| iree_status_t status_ = iree_ok_status(); | |
| }; | |
| // Owns the VM and HAL state of one tool run: loads the modules, discovers the | |
| // workloads, runs them for Google Benchmark and processes their outputs. | |
| class BenchmarkSession { | |
| public: | |
| BenchmarkSession() { iree_tooling_module_list_initialize(&modules_); } | |
| ~BenchmarkSession() { iree_status_ignore(Close()); } | |
| iree_status_t Open() { | |
| if (FLAG_batch_size <= 0 || FLAG_batch_concurrency <= 0) { | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "batch size and concurrency must be positive"); | |
| } | |
| if (FLAG_dispatch_statistics) { | |
| // Reject conflicts before any capture or profiling session starts. | |
| bool profiling_requested = false; | |
| IREE_RETURN_IF_ERROR( | |
| iree_hal_profiling_from_flags_is_requested(&profiling_requested)); | |
| if (profiling_requested) { | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "--dispatch_statistics cannot be combined with " | |
| "device profiling or capture flags"); | |
| } | |
| // The profiling pass would add untimed executions to the capture. | |
| if (iree_tooling_device_replay_capture_requested()) { | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "--dispatch_statistics cannot be combined with " | |
| "--device_replay_output"); | |
| } | |
| } | |
| iree_allocator_t allocator = iree_allocator_system(); | |
| IREE_RETURN_IF_ERROR(iree_tooling_create_instance(allocator, &instance_)); | |
| IREE_RETURN_IF_ERROR(iree_tooling_load_modules_from_flags( | |
| instance_.get(), allocator, &modules_)); | |
| if (!modules_.count) { | |
| return iree_make_status( | |
| IREE_STATUS_INVALID_ARGUMENT, | |
| "no user module specified; use --module=file.vmfb"); | |
| } | |
| IREE_RETURN_IF_ERROR(iree_tooling_create_context_from_flags( | |
| instance_.get(), modules_.count, modules_.values, | |
| iree_string_view_empty(), allocator, &context_, &devices_, &allocator_, | |
| &recorder_)); | |
| const bool selected = strlen(FLAG_function) != 0; | |
| iree_vm_module_t* module = iree_tooling_module_list_back(&modules_); | |
| if (selected) { | |
| iree_vm_function_t function; | |
| IREE_RETURN_IF_ERROR(iree_vm_module_lookup_function_by_name( | |
| module, IREE_VM_FUNCTION_LINKAGE_EXPORT, | |
| iree_make_cstring_view(FLAG_function), &function)); | |
| IREE_RETURN_IF_ERROR(AddWorkload(function, true)); | |
| } else { | |
| iree_vm_module_signature_t signature = iree_vm_module_signature(module); | |
| for (iree_host_size_t i = 0; i < signature.export_function_count; ++i) { | |
| iree_vm_function_t function; | |
| IREE_RETURN_IF_ERROR(iree_vm_module_lookup_function_by_ordinal( | |
| module, IREE_VM_FUNCTION_LINKAGE_EXPORT, i, &function)); | |
| IREE_RETURN_IF_ERROR(AddWorkload(function, false)); | |
| } | |
| } | |
| if (FLAG_enable_output_processing && workloads_.size() != 1) { | |
| return iree_make_status( | |
| IREE_STATUS_INVALID_ARGUMENT, | |
| "output processing requires one function; use --function"); | |
| } | |
| if (!FLAG_dispatch_statistics) { | |
| return iree_hal_begin_device_list_profiling_from_flags( | |
| devices_ ? devices_->count : 0, | |
| devices_ ? devices_->devices : nullptr, allocator, &profiling_); | |
| } | |
| if (!devices_ || !devices_->count) { | |
| return iree_make_status( | |
| IREE_STATUS_INVALID_ARGUMENT, | |
| "--dispatch_statistics requires a module that uses HAL devices"); | |
| } | |
| profiler_ = std::make_unique<DispatchProfiler>(devices_); | |
| return profiler_->Probe(); | |
| } | |
| void Register() { | |
| for (auto& workload : workloads_) { | |
| Workload* target = workload.get(); | |
| benchmark::RegisterBenchmark( | |
| target->name.c_str(), | |
| [this, target](benchmark::State& state) { Run(*target, state); }) | |
| ->MeasureProcessCPUTime() | |
| ->UseRealTime() | |
| ->Unit(target->unit); | |
| } | |
| } | |
| iree_status_t ProcessOutputs(int* exit_code) { | |
| if (!FLAG_enable_output_processing) return iree_ok_status(); | |
| for (auto& workload : workloads_) { | |
| if (!workload->outputs) continue; // Filtered/listed without execution. | |
| if (Device()) { | |
| iree_hal_buffer_params_t params = {}; | |
| params.usage = | |
| IREE_HAL_BUFFER_USAGE_TRANSFER | IREE_HAL_BUFFER_USAGE_MAPPING; | |
| params.access = IREE_HAL_MEMORY_ACCESS_ALL; | |
| params.type = IREE_HAL_MEMORY_TYPE_HOST_LOCAL | | |
| IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE; | |
| params.queue_affinity = IREE_HAL_QUEUE_AFFINITY_ANY; | |
| IREE_RETURN_IF_ERROR(iree_tooling_transfer_variants( | |
| workload->outputs.get(), Device(), allocator_.get(), params, | |
| nullptr, nullptr)); | |
| } | |
| IREE_RETURN_IF_ERROR(iree_tooling_process_results_and_print( | |
| Device(), | |
| iree_make_string_view(workload->results_cconv.data(), | |
| workload->results_cconv.size()), | |
| workload->outputs.get(), iree_allocator_system(), exit_code)); | |
| } | |
| return iree_ok_status(); | |
| } | |
| iree_status_t Close() { | |
| iree_status_t status = | |
| iree_hal_profiling_session_end(std::exchange(profiling_, nullptr)); | |
| profiler_.reset(); | |
| workloads_.clear(); | |
| context_.reset(); | |
| iree_tooling_module_list_reset(&modules_); | |
| instance_.reset(); | |
| if (recorder_) { | |
| status = | |
| iree_status_join(status, iree_hal_replay_recorder_close(recorder_)); | |
| iree_hal_replay_recorder_release(recorder_); | |
| recorder_ = nullptr; | |
| } | |
| if (FLAG_print_statistics && devices_) { | |
| for (iree_host_size_t i = 0; i < devices_->count; ++i) { | |
| iree_hal_device_t* device = devices_->devices[i]; | |
| iree_string_view_t device_id = iree_hal_device_id(device); | |
| fprintf(stderr, "Device: %.*s\n", (int)device_id.size, device_id.data); | |
| status = iree_status_join( | |
| status, iree_hal_allocator_statistics_fprint( | |
| stderr, iree_hal_device_allocator(device))); | |
| fprintf(stderr, "\n"); | |
| } | |
| } | |
| allocator_.reset(); | |
| iree_hal_device_list_free(devices_); | |
| devices_ = nullptr; | |
| return status; | |
| } | |
| // Ends the session of the device profiling flags so that output processing | |
| // is not profiled, and returns the failures of all benchmark runs. | |
| iree_status_t FinishMeasurements() { | |
| iree_status_t status = std::exchange(error_, iree_ok_status()); | |
| if (profiler_) { | |
| status = iree_status_join(status, profiler_->ConsumeStatus()); | |
| } | |
| return iree_status_join(status, iree_hal_profiling_session_end( | |
| std::exchange(profiling_, nullptr))); | |
| } | |
| // Null unless --dispatch_statistics is set. | |
| DispatchProfiler* Profiler() const { return profiler_.get(); } | |
| const std::vector<std::unique_ptr<Workload>>& Workloads() const { | |
| return workloads_; | |
| } | |
| private: | |
| iree_hal_device_t* Device() const { | |
| return devices_ && devices_->count ? devices_->devices[0] : nullptr; | |
| } | |
| iree_status_t AddWorkload(iree_vm_function_t function, bool selected) { | |
| iree_string_view_t name = iree_vm_function_name(&function); | |
| iree_string_view_t type = iree_vm_function_lookup_attr_by_name( | |
| &function, IREE_SV("iree.benchmark")); | |
| iree_string_view_t model = iree_vm_function_lookup_attr_by_name( | |
| &function, IREE_SV("iree.abi.model")); | |
| InvocationKind kind = InvocationKind::kSync; | |
| if (iree_string_view_equal(type, IREE_SV("dispatch"))) { | |
| kind = InvocationKind::kDispatch; | |
| } else if (iree_string_view_equal(model, IREE_SV("coarse-fences"))) { | |
| kind = InvocationKind::kAsync; | |
| } | |
| // Without --function, run benchmark wrappers and any other public function | |
| // whose arguments the tool can supply without --input. | |
| const bool discovered = !selected && kind != InvocationKind::kDispatch && | |
| !iree_string_view_equal(type, IREE_SV("entry")); | |
| if (discovered && | |
| (iree_string_view_starts_with(name, IREE_SV("__")) || | |
| iree_string_view_find_char(name, '$', 0) != IREE_STRING_VIEW_NPOS)) { | |
| return iree_ok_status(); // Internal or special function. | |
| } | |
| iree_vm_function_signature_t signature = | |
| iree_vm_function_signature(&function); | |
| if (discovered) { | |
| iree_host_size_t argument_count = 0, result_count = 0; | |
| IREE_RETURN_IF_ERROR(iree_vm_function_call_count_arguments_and_results( | |
| &signature, &argument_count, &result_count)); | |
| if (argument_count != (kind == InvocationKind::kAsync ? 2u : 0u)) { | |
| return iree_ok_status(); | |
| } | |
| } | |
| iree_string_view_t arguments, results; | |
| IREE_RETURN_IF_ERROR(iree_vm_function_call_get_cconv_fragments( | |
| &signature, &arguments, &results)); | |
| auto workload = std::make_unique<Workload>(); | |
| workload->name = "BM_" + std::string(name.data, name.size); | |
| workload->function = function; | |
| workload->kind = kind; | |
| workload->results_cconv.assign(results.data, results.size); | |
| workload->batch_size = FLAG_batch_size; | |
| workload->concurrency = | |
| kind == InvocationKind::kAsync ? FLAG_batch_concurrency : 1; | |
| // Concurrency need not be a power of two. Keep the logical iteration count | |
| // consistent with the actual number of invocations in the rounded batch. | |
| int64_t batch = | |
| ((int64_t)workload->batch_size + workload->concurrency - 1) / | |
| workload->concurrency * workload->concurrency; | |
| if (batch > INT32_MAX) { | |
| return iree_make_status(IREE_STATUS_OUT_OF_RANGE, | |
| "rounded batch size is too large"); | |
| } | |
| workload->batch_size = static_cast<int32_t>(batch); | |
| workload->unit = FLAG_time_unit.first ? FLAG_time_unit.second | |
| : kind == InvocationKind::kDispatch | |
| ? benchmark::kMicrosecond | |
| : benchmark::kMillisecond; | |
| if (kind == InvocationKind::kDispatch) { | |
| if (selected && FLAG_input_list().count) { | |
| return iree_make_status( | |
| IREE_STATUS_INVALID_ARGUMENT, | |
| "dispatch wrappers take --batch_size, not --input"); | |
| } | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_create(iree_vm_make_undefined_type_def(), 1, | |
| iree_allocator_system(), &workload->inputs)); | |
| iree_vm_value_t count = iree_vm_value_make_i32(workload->batch_size); | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_push_value(workload->inputs.get(), &count)); | |
| } else if (selected) { | |
| IREE_RETURN_IF_ERROR(iree_tooling_parse_variants( | |
| arguments, FLAG_input_list(), Device(), allocator_.get(), | |
| iree_allocator_system(), &workload->inputs)); | |
| } else { | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_create(iree_vm_make_undefined_type_def(), 2, | |
| iree_allocator_system(), &workload->inputs)); | |
| } | |
| workloads_.push_back(std::move(workload)); | |
| return iree_ok_status(); | |
| } | |
| bool Check(iree_status_t status) { | |
| if (iree_status_is_ok(status)) return true; | |
| if (state_) { | |
| state_->SkipWithError(iree::Status(iree_status_clone(status)).ToString()); | |
| } | |
| error_ = iree_status_join(error_, status); | |
| return false; | |
| } | |
| // Preserve values even when outputs alias inputs or mutable module storage. | |
| // Run after measurement stops and before the profiling pass can overwrite | |
| // them. Host-accessible outputs still need a copy; a transfer that only | |
| // ensures host accessibility may leave them aliasing the original storage. | |
| iree_status_t SnapshotOutputs(Workload& workload) { | |
| if (!workload.outputs) return iree_ok_status(); | |
| return SnapshotList(workload.outputs.get()); | |
| } | |
| // Replaces the buffers, buffer views and lists in |list| with copies. Nested | |
| // lists may be retained and mutated by the module, so they are cloned before | |
| // their elements are replaced; only |list| itself must be owned by the tool. | |
| iree_status_t SnapshotList(iree_vm_list_t* list) { | |
| iree_hal_buffer_params_t params = {}; | |
| params.usage = | |
| IREE_HAL_BUFFER_USAGE_TRANSFER | IREE_HAL_BUFFER_USAGE_MAPPING; | |
| params.access = IREE_HAL_MEMORY_ACCESS_ALL; | |
| params.type = | |
| IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE; | |
| params.queue_affinity = IREE_HAL_QUEUE_AFFINITY_ANY; | |
| for (iree_host_size_t i = 0; i < iree_vm_list_size(list); ++i) { | |
| iree_vm_variant_t value = iree_vm_variant_empty(); | |
| IREE_RETURN_IF_ERROR(iree_vm_list_get_variant_assign(list, i, &value)); | |
| if (!iree_vm_variant_is_ref(value) || !value.ref.ptr) continue; | |
| if (iree_vm_list_isa(value.ref)) { | |
| vm::ref<iree_vm_list_t> copy; | |
| IREE_RETURN_IF_ERROR(iree_vm_list_clone( | |
| iree_vm_list_deref(value.ref), iree_allocator_system(), ©)); | |
| IREE_RETURN_IF_ERROR(SnapshotList(copy.get())); | |
| IREE_RETURN_IF_ERROR(iree_vm_list_set_ref_retain(list, i, copy)); | |
| continue; | |
| } | |
| iree_hal_buffer_view_t* source_view = nullptr; | |
| iree_hal_buffer_t* source = nullptr; | |
| if (iree_hal_buffer_view_isa(value.ref)) { | |
| source_view = iree_hal_buffer_view_deref(value.ref); | |
| source = iree_hal_buffer_view_buffer(source_view); | |
| } else if (iree_hal_buffer_isa(value.ref)) { | |
| source = iree_hal_buffer_deref(value.ref); | |
| } else { | |
| continue; | |
| } | |
| const iree_device_size_t length = iree_hal_buffer_byte_length(source); | |
| vm::ref<iree_hal_buffer_t> copy; | |
| IREE_RETURN_IF_ERROR(iree_hal_allocator_allocate_buffer( | |
| allocator_.get(), params, length, ©)); | |
| IREE_RETURN_IF_ERROR(iree_hal_device_transfer_d2d( | |
| Device(), source, 0, copy.get(), 0, length, | |
| IREE_HAL_TRANSFER_BUFFER_FLAG_DEFAULT, iree_infinite_timeout())); | |
| if (source_view) { | |
| vm::ref<iree_hal_buffer_view_t> copy_view; | |
| IREE_RETURN_IF_ERROR(iree_hal_buffer_view_create_like( | |
| copy.get(), source_view, iree_allocator_system(), ©_view)); | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_set_buffer_view_retain(list, i, copy_view.get())); | |
| } else { | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_set_buffer_retain(list, i, copy.get())); | |
| } | |
| } | |
| return iree_ok_status(); | |
| } | |
| void Run(Workload& workload, benchmark::State& state) { | |
| state_ = &state; | |
| if (profiler_) profiler_->Attach(&workload, &state); | |
| const bool async = workload.kind == InvocationKind::kAsync; | |
| Batch batch; | |
| if (!iree_status_is_ok(error_) || (profiler_ && profiler_->Failed())) { | |
| state.SkipWithError("an earlier benchmark or capture failed"); | |
| } else if (!async) { | |
| Check(batch.Prepare(workload, Device())); | |
| } | |
| IREE_TRACE_ZONE_BEGIN_NAMED_DYNAMIC(z0, workload.name.data(), | |
| workload.name.size()); | |
| IREE_TRACE_FRAME_MARK(); | |
| while (state.KeepRunningBatch(workload.batch_size)) { | |
| if (async) { | |
| state.PauseTiming(); | |
| bool prepared = Check(batch.Prepare(workload, Device())); | |
| if (!prepared) continue; | |
| state.ResumeTiming(); | |
| } | |
| IREE_TRACE_ZONE_BEGIN_NAMED(z1, "BenchmarkIteration"); | |
| IREE_TRACE_FRAME_MARK_NAMED("Iteration"); | |
| iree_status_t status = recorder_ ? iree_hal_replay_recorder_scope_begin( | |
| recorder_, IREE_SV("execute")) | |
| : iree_ok_status(); | |
| if (iree_status_is_ok(status)) { | |
| status = batch.Execute(workload, context_.get()); | |
| if (recorder_) { | |
| status = iree_status_join(status, iree_hal_replay_recorder_scope_end( | |
| recorder_, IREE_SV("execute"))); | |
| } | |
| } | |
| IREE_TRACE_ZONE_END(z1); | |
| if (!Check(status)) continue; | |
| // Outputs belong to the measured pass; the profiling pass reruns it. | |
| const bool profiling_pass = profiler_ && profiler_->InPass(); | |
| iree_hal_profiling_session_t* profiling = | |
| profiling_pass ? profiler_->Session() : profiling_; | |
| const bool pause = async || profiling || | |
| (workload.kind == InvocationKind::kSync && Device()); | |
| if (pause) state.PauseTiming(); | |
| if (async) { | |
| if (FLAG_enable_output_processing && !profiling_pass) { | |
| workload.outputs = vm::retain_ref(batch.invocations.back().outputs); | |
| } | |
| batch = {}; | |
| } | |
| if (pause && Check(iree_hal_profiling_session_flush(profiling))) { | |
| state.ResumeTiming(); | |
| } | |
| } | |
| state.SetItemsProcessed(state.iterations()); | |
| const bool profiling_pass = profiler_ && profiler_->InPass(); | |
| if (!async && FLAG_enable_output_processing && !profiling_pass && | |
| !state.skipped()) { | |
| workload.outputs = std::move(batch.invocations.front().outputs); | |
| } | |
| if (profiler_ && FLAG_enable_output_processing && !profiling_pass && | |
| !state.skipped()) { | |
| Check(SnapshotOutputs(workload)); | |
| } | |
| IREE_TRACE_ZONE_END(z0); | |
| if (profiler_) profiler_->Detach(); | |
| state_ = nullptr; | |
| } | |
| vm::ref<iree_vm_instance_t> instance_; | |
| vm::ref<iree_vm_context_t> context_; | |
| // Allocator of the first device, used for inputs and output transfers. | |
| vm::ref<iree_hal_allocator_t> allocator_; | |
| // Modules loaded from --module; the last one is benchmarked. | |
| iree_tooling_module_list_t modules_; | |
| // Devices the context uses; NULL or empty if the program uses none. | |
| iree_hal_device_list_t* devices_ = nullptr; | |
| // Recorder of --device_replay_output, or NULL. | |
| iree_hal_replay_recorder_t* recorder_ = nullptr; | |
| // Session requested by the device profiling flags, if any. | |
| iree_hal_profiling_session_t* profiling_ = nullptr; | |
| // Profiler of --dispatch_statistics, or NULL. | |
| std::unique_ptr<DispatchProfiler> profiler_; | |
| // Functions to benchmark, in registration order. | |
| std::vector<std::unique_ptr<Workload>> workloads_; | |
| // State of the running benchmark function, or NULL between functions. | |
| benchmark::State* state_ = nullptr; | |
| // Failures of the measured runs, reported after all benchmarks finish. | |
| iree_status_t error_ = iree_ok_status(); | |
| }; | |
| // Reporting owns no executable callbacks. Buffer the completed reports so all | |
| // names and counter columns are known before a console/CSV header is emitted. | |
| // Both display and file adapters read the same immutable profiling results. | |
| class DispatchReporter : public benchmark::BenchmarkReporter { | |
| public: | |
| DispatchReporter(benchmark::BenchmarkReporter* reporter, | |
| const BenchmarkSession& session) | |
| : reporter_(reporter), session_(session) {} | |
| bool ReportContext(const Context& context) override { | |
| context_.emplace(context); | |
| return true; | |
| } | |
| void ReportRuns(const std::vector<Run>& runs) override { | |
| runs_.insert(runs_.end(), runs.begin(), runs.end()); | |
| } | |
| void Finalize() override { | |
| if (!context_) return; | |
| std::vector<Run> reports; | |
| for (size_t i = 0; i < runs_.size(); ++i) { | |
| const auto& base = runs_[i]; | |
| reports.push_back(base); | |
| if (i + 1 < runs_.size() && | |
| runs_[i + 1].run_name.function_name == base.run_name.function_name) { | |
| continue; | |
| } | |
| for (const auto& workload : session_.Workloads()) { | |
| if (workload->name == base.run_name.function_name) { | |
| Append(*workload, base, reports); | |
| } | |
| } | |
| } | |
| for (const auto& run : reports) { | |
| context_->name_field_width = | |
| std::max(context_->name_field_width, run.benchmark_name().size()); | |
| } | |
| reporter_->SetOutputStream(&GetOutputStream()); | |
| reporter_->SetErrorStream(&GetErrorStream()); | |
| if (reporter_->ReportContext(*context_)) reporter_->ReportRuns(reports); | |
| reporter_->Finalize(); | |
| } | |
| private: | |
| static void Append(const Workload& workload, const Run& base, | |
| std::vector<Run>& reports) { | |
| const DispatchProfile& profile = workload.profile; | |
| if (!profile.iterations) return; | |
| std::vector<std::pair<std::string, DispatchRow>> rows(profile.rows.begin(), | |
| profile.rows.end()); | |
| std::stable_sort(rows.begin(), rows.end(), | |
| [](const auto& a, const auto& b) { | |
| return a.second.duration_ns > b.second.duration_ns; | |
| }); | |
| uint64_t total = 0; | |
| for (const auto& row : rows) total += row.second.duration_ns; | |
| for (const auto& entry : rows) { | |
| const auto& row = entry.second; | |
| Run run; | |
| run.run_name.function_name = workload.name; | |
| run.run_name.args = entry.first; | |
| run.family_index = base.family_index; | |
| run.per_family_instance_index = base.per_family_instance_index; | |
| run.repetition_index = 0; | |
| run.repetitions = 1; | |
| run.iterations = profile.iterations; | |
| run.time_unit = workload.unit; | |
| run.real_accumulated_time = row.duration_ns * 1e-9; | |
| run.cpu_accumulated_time = row.tile_duration_ns * 1e-9; | |
| run.counters["calls"] = static_cast<double>(row.calls) / run.iterations; | |
| run.counters["percent"] = total ? 100.0 * row.duration_ns / total : 0; | |
| reports.push_back(std::move(run)); | |
| } | |
| } | |
| // Formatter that writes the merged reports. | |
| std::unique_ptr<benchmark::BenchmarkReporter> reporter_; | |
| // Provides the workloads and their dispatch profiles. | |
| const BenchmarkSession& session_; | |
| // Context and measured runs buffered until Finalize. | |
| std::optional<Context> context_; | |
| std::vector<Run> runs_; | |
| }; | |
| // Creates the reporter Google Benchmark would use for --benchmark_out, whose | |
| // format Initialize has validated. | |
| static benchmark::BenchmarkReporter* CreateFileReporter() { | |
| const std::string& format = ::benchmark::FLAGS_benchmark_out_format; | |
| if (format == "json") return new benchmark::JSONReporter; | |
| BENCHMARK_DISABLE_DEPRECATED_WARNING | |
| if (format == "csv") return new benchmark::CSVReporter; | |
| BENCHMARK_RESTORE_DEPRECATED_WARNING | |
| return new benchmark::ConsoleReporter( | |
| ::benchmark::FLAGS_benchmark_counters_tabular | |
| ? benchmark::ConsoleReporter::OO_Tabular | |
| : benchmark::ConsoleReporter::OO_None); | |
| } | |
| static int RunBenchmarkModule(int argc, char** argv) { | |
| iree_flags_set_usage("iree-benchmark-module", kIreeBenchmarkModuleUsage); | |
| iree_flags_parse_checked(IREE_FLAGS_PARSE_MODE_UNDEFINED_OK | | |
| IREE_FLAGS_PARSE_MODE_CONTINUE_AFTER_HELP, | |
| &argc, &argv); | |
| if (FLAG_agents_md) { | |
| PrintBenchmarkModuleAgentMarkdown(stdout); | |
| return EXIT_SUCCESS; | |
| } | |
| ::benchmark::Initialize(&argc, argv); | |
| BenchmarkSession session; | |
| iree_status_t status = session.Open(); | |
| int exit_code = EXIT_SUCCESS; | |
| if (iree_status_is_ok(status)) { | |
| session.Register(); | |
| std::unique_ptr<DispatchReporter> display, file; | |
| if (DispatchProfiler* profiler = session.Profiler()) { | |
| ::benchmark::RegisterProfilerManager(profiler); | |
| display = std::make_unique<DispatchReporter>( | |
| ::benchmark::CreateDefaultDisplayReporter(), session); | |
| if (!::benchmark::FLAGS_benchmark_out.empty()) { | |
| file = | |
| std::make_unique<DispatchReporter>(CreateFileReporter(), session); | |
| } | |
| } | |
| ::benchmark::RunSpecifiedBenchmarks(display.get(), file.get()); | |
| ::benchmark::RegisterProfilerManager(nullptr); | |
| ::benchmark::ClearRegisteredBenchmarks(); | |
| status = session.FinishMeasurements(); | |
| if (iree_status_is_ok(status)) status = session.ProcessOutputs(&exit_code); | |
| for (const auto& workload : session.Workloads()) { | |
| const DispatchProfile& profile = workload->profile; | |
| if (!profile.iterations) continue; | |
| if (profile.rows.empty() || profile.dropped_records || | |
| profile.unscaled_rows) { | |
| fprintf(stderr, | |
| "--dispatch_statistics: %s: %zu functions, %" PRIu64 | |
| " dropped records, %" PRIu64 | |
| " unscaled rows; timings may be incomplete\n", | |
| workload->name.c_str(), profile.rows.size(), | |
| profile.dropped_records, profile.unscaled_rows); | |
| } | |
| } | |
| } | |
| status = iree_status_join(status, session.Close()); | |
| if (!iree_status_is_ok(status)) { | |
| iree_status_fprint(stderr, status); | |
| iree_status_free(status); | |
| return EXIT_FAILURE; | |
| } | |
| return exit_code; | |
| } | |
| } // namespace | |
| } // namespace iree | |
| int main(int argc, char** argv) { | |
| IREE_TRACE_APP_ENTER(); | |
| IREE_TRACE_ZONE_BEGIN_NAMED(z0, "iree-benchmark-module"); | |
| int exit_code = iree::RunBenchmarkModule(argc, argv); | |
| IREE_TRACE_ZONE_END(z0); | |
| IREE_TRACE_APP_EXIT(exit_code); | |
| return exit_code; | |
| } |