Loading...
Searching...
No Matches
profiler.h
Go to the documentation of this file.
1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Artificial Intelligence Techniques, S.L. and Roberto López González
3
4#pragma once
5
6#include <atomic>
7#include <cstdlib>
8#include <chrono>
9#include <iomanip>
10#include <iostream>
11#include <map>
12#include <mutex>
13#include <string>
14#include <string_view>
15#include <utility>
16#include <vector>
17
19#include "opennn/core/log.h"
20
22{
23
24// Achievable bandwidth in GB/s, used only to express a scope as a percentage of
25// peak. Set OPENNN_PEAK_GBPS from a STREAM-style triad on the machine that is
26// measuring; a spec-sheet figure is never reached in practice and would make
27// every kernel look worse than it is. Unset means the percentage is omitted.
28inline double peak_gbps()
29{
30 static const double value = []
31 {
32 const char* const text = std::getenv("OPENNN_PEAK_GBPS");
33 if (text == nullptr) return 0.0;
34
35 const double parsed = std::atof(text);
36 return parsed > 0.0 ? parsed : 0.0;
37 }();
38
39 return value;
40}
41
42class Stats
43{
44public:
45 struct Entry { double total_ms = 0.0; long calls = 0; double total_bytes = 0.0; };
46
47 void add(const string& key, double ms, double bytes = 0.0)
48 {
49 const std::lock_guard lock(entries_mutex);
50 Entry& entry = entries[key];
51 entry.total_ms += ms;
52 entry.total_bytes += bytes;
53 ++entry.calls;
54 }
55
56 void set(const string& key, double total_ms, long calls, double total_bytes = 0.0)
57 {
58 if (calls <= 0) return;
59
60 const std::lock_guard lock(entries_mutex);
61 entries[key] = {total_ms, calls, total_bytes};
62 }
63
64 void clear()
65 {
66 const std::lock_guard lock(entries_mutex);
67 entries.clear();
68 }
69
70 double total_ms() const
71 {
72 const std::lock_guard lock(entries_mutex);
73
74 double total = 0.0;
75 for (const auto& entry : entries)
76 total += entry.second.total_ms;
77
78 return total;
79 }
80
81 long call_count(const string& key) const
82 {
83 const std::lock_guard lock(entries_mutex);
84 const auto found = entries.find(key);
85 return found == entries.end() ? 0 : found->second.calls;
86 }
87
88 void log(const string& title, double total_ms = 0.0,
89 string_view category = "PROFILE") const
90 {
92 ostringstream output;
93 print(output, title, total_ms, category);
95 }
96
97 void print(ostream& os,
98 const string& title,
99 double total_ms = 0.0,
100 string_view category = "PROFILE") const
101 {
102 vector<pair<string, Entry>> sorted;
103 {
104 const std::lock_guard lock(entries_mutex);
105 sorted.assign(entries.begin(), entries.end());
106 }
107 ranges::sort(sorted, greater<>{}, [](const auto& entry) { return entry.second.total_ms; });
108
109 bool any_bytes = false;
110 for (const auto& entry : sorted)
111 if (entry.second.total_bytes > 0.0) { any_bytes = true; break; }
112
113 const double peak = peak_gbps();
114
115 os << "\n[" << category << "] " << title << "\n";
116 os << " " << left << setw(48) << "section"
117 << right << setw(12) << "total_ms"
118 << setw(10) << "calls"
119 << setw(12) << "ms/call";
120 if (total_ms > 0.0) os << setw(8) << "%";
121 if (any_bytes)
122 {
123 os << setw(14) << "GiB" << setw(10) << "GB/s";
124 if (peak > 0.0) os << setw(8) << "%peak";
125 }
126 os << "\n";
127
128 for (const auto& [key, entry] : sorted)
129 {
130 os << " " << left << setw(48) << key
131 << right << setw(12) << fixed << setprecision(2) << entry.total_ms
132 << setw(10) << entry.calls
133 << setw(12) << fixed << setprecision(3) << (entry.total_ms / double(entry.calls));
134 if (total_ms > 0.0)
135 os << setw(7) << fixed << setprecision(1) << (entry.total_ms / total_ms * 100.0) << "%";
136
137 if (any_bytes)
138 {
139 const bool measured = entry.total_bytes > 0.0 && entry.total_ms > 0.0;
140
141 // Sizes are binary GiB to match every other size OpenNN reports;
142 // the rate is decimal GB/s, which is how bandwidth is quoted.
143 const double gbps = measured ? entry.total_bytes / (entry.total_ms * 1.0e6) : 0.0;
144
145 // Six decimals so a kernel moving a few hundred kilobytes still
146 // reads as a number rather than 0.000, which is the case that
147 // matters when chasing a small kernel that runs a great many
148 // times.
149 if (measured)
150 os << setw(14) << fixed << setprecision(6) << (entry.total_bytes / 1073741824.0)
151 << setw(10) << fixed << setprecision(1) << gbps;
152 else
153 os << setw(14) << "-" << setw(10) << "-";
154
155 if (peak > 0.0)
156 {
157 // Nothing beats the memory system, so a rate above peak
158 // means the scope and its byte count disagree about what
159 // happened. Two ways to get here. The scope may be timing
160 // less work than it is charged for, which is what an
161 // operator fused into its predecessor looks like: the call
162 // returns almost immediately while the model still charges
163 // it a full pass. Or the byte model is simply wrong. Either
164 // way the number is not a bandwidth, so refuse to print one.
165 //
166 // A scope on a CUDA-graph path is a different problem and
167 // does not land here: it reports honestly, but only for the
168 // eager warmup calls, since capture disables the profiler
169 // and the replays never enter the scope at all. Set
170 // OPENNN_GRAPH_TIMING to keep the run eager when what you
171 // want is bandwidth rather than throughput.
172 if (measured && gbps <= peak)
173 os << setw(7) << fixed << setprecision(1) << (gbps / peak * 100.0) << "%";
174 else if (measured)
175 os << setw(8) << ">peak";
176 else
177 os << setw(8) << "-";
178 }
179 }
180 os << "\n";
181 }
182 os << "\n";
183 }
184
185private:
186 map<string, Entry> entries;
187 mutable std::mutex entries_mutex;
188};
189
190inline Stats& stats()
191{
192 static Stats instance;
193 return instance;
194}
195
196namespace detail
197{
198
199inline std::atomic_bool& enabled_flag()
200{
201 static std::atomic_bool enabled{std::getenv("OPENNN_PROFILE") != nullptr};
202 return enabled;
203}
204
206{
208 ~ExitDump();
209};
210
212{
213 static ExitDump dump;
214 return dump;
215}
216
217}
218
219inline bool is_enabled() noexcept
220{
221 return detail::enabled_flag().load(std::memory_order_relaxed);
222}
223
224inline void set_enabled(bool enabled) noexcept
225{
226 detail::enabled_flag().store(enabled, std::memory_order_relaxed);
227}
228
230{
231public:
232 ScopedTimer(string new_key, bool synchronize_gpu = true, double moved_bytes = 0.0)
233 : key(std::move(new_key)),
234 bytes(moved_bytes),
235 sync_gpu(synchronize_gpu),
236 active(is_enabled() && !key.empty())
237 {
238 if (!active) return;
240 if (sync_gpu) device::synchronize();
241 start = chrono::steady_clock::now();
242 }
243
244 // For a scope that only learns its working set partway through, such as one
245 // that picks between a fused and a materialized path after it has started.
246 void set_bytes(double moved_bytes) noexcept { bytes = moved_bytes; }
247
249 {
250 if (!active) return;
251 if (sync_gpu) device::synchronize();
252 const auto end_time = chrono::steady_clock::now();
253 const double elapsed_ms = chrono::duration<double, milli>(end_time - start).count();
254 stats().add(key, elapsed_ms, bytes);
255 }
256
257 ScopedTimer(const ScopedTimer&) = delete;
259
260private:
261 string key;
262 double bytes;
263 chrono::steady_clock::time_point start;
264 bool sync_gpu;
265 bool active;
266};
267
268namespace detail
269{
270
272{
273 if (is_enabled() && stats().total_ms() > 0.0)
274 stats().log("profile (OPENNN_PROFILE)");
275}
276
277}
278
279}
280
281#define OPENNN_PROFILE_CAT_INNER(a, b) a##b
282#define OPENNN_PROFILE_CAT(a, b) OPENNN_PROFILE_CAT_INNER(a, b)
283
284#define PROFILE_SCOPE_IMPL(name, sync) \
285 ::opennn::profiler::ScopedTimer OPENNN_PROFILE_CAT(_profile_, __LINE__)( \
286 ::opennn::profiler::is_enabled() ? string(name) : string{}, sync)
287
288#define PROFILE_SCOPE(name) PROFILE_SCOPE_IMPL(name, true)
289#define PROFILE_SCOPE_HOST(name) PROFILE_SCOPE_IMPL(name, false)
290
291// Like PROFILE_SCOPE, but also records how many bytes the scope moves so the
292// report can put achieved bandwidth next to the time. Pass every byte the
293// memory system sees -- reads plus writes, counting a tensor once per pass over
294// it, not once in total.
295#define PROFILE_SCOPE_BYTES(name, bytes) \
296 ::opennn::profiler::ScopedTimer OPENNN_PROFILE_CAT(_profile_, __LINE__)( \
297 ::opennn::profiler::is_enabled() ? string(name) : string{}, true, double(bytes))
298
299// A named timer, for when the byte count is only known after the scope opens.
300// Call timer.set_bytes(...) once the path is chosen.
301#define PROFILE_SCOPE_NAMED(timer, name) \
302 ::opennn::profiler::ScopedTimer timer( \
303 ::opennn::profiler::is_enabled() ? string(name) : string{}, true)
ScopedTimer(const ScopedTimer &)=delete
ScopedTimer & operator=(const ScopedTimer &)=delete
void set_bytes(double moved_bytes) noexcept
Definition profiler.h:246
~ScopedTimer()
Definition profiler.h:248
ScopedTimer(string new_key, bool synchronize_gpu=true, double moved_bytes=0.0)
Definition profiler.h:232
Definition profiler.h:43
void clear()
Definition profiler.h:64
void set(const string &key, double total_ms, long calls, double total_bytes=0.0)
Definition profiler.h:56
void log(const string &title, double total_ms=0.0, string_view category="PROFILE") const
Definition profiler.h:88
void add(const string &key, double ms, double bytes=0.0)
Definition profiler.h:47
void print(ostream &os, const string &title, double total_ms=0.0, string_view category="PROFILE") const
Definition profiler.h:97
double total_ms() const
Definition profiler.h:70
long call_count(const string &key) const
Definition profiler.h:81
void synchronize(DeviceStream=nullptr)
void write(Level, std::string_view)
bool enabled(Level) noexcept
@ Info
Definition log.h:26
Definition profiler.h:197
ExitDump & exit_dump()
Definition profiler.h:211
std::atomic_bool & enabled_flag()
Definition profiler.h:199
Definition profiler.h:22
double peak_gbps()
Definition profiler.h:28
Stats & stats()
Definition profiler.h:190
void set_enabled(bool enabled) noexcept
Definition profiler.h:224
bool is_enabled() noexcept
Definition profiler.h:219
Definition profiler.h:45
long calls
Definition profiler.h:45
double total_bytes
Definition profiler.h:45
double total_ms
Definition profiler.h:45
Definition profiler.h:206
ExitDump()
Definition profiler.h:207
~ExitDump()
Definition profiler.h:271