Loading...
Searching...
No Matches
cudnn_frontend_utilities.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#ifdef OPENNN_HAS_CUDA
7
8#include <cudnn_frontend.h>
9#include "opennn/core/log.h"
10
11#include <algorithm>
12#include <filesystem>
13#include <fstream>
14#include <string_view>
15
21
23{
24using namespace ::cudnn_frontend;
25
26inline constexpr size_t graph_cache_capacity = 8;
27
28inline const auto check_status = [](auto status, const string& what) {
29 throw_if(status.is_bad(),
30 "cudnn-frontend {}: {}", what, status.get_message());
31};
32
34{
35 // Encoded as 100 * major + 10 * minor (8.6 -> 860, 12.0 -> 1200); the
36 // plan-cache directory name embeds it, so the encoding must stay fixed.
37 // The device current on the first caller's thread is queried once and the
38 // static serves every later call, since frontend_enabled() sits on hot
39 // paths. A failed query leaves a zero component and disables the frontend.
40 struct QueriedCapability
41 {
42 int encoded = 0;
43
44 static int component_of(const int device_ordinal, const cudaDeviceAttr attribute)
45 {
46 int component = 0;
47 cudaDeviceGetAttribute(&component, attribute, device_ordinal);
48 return component;
49 }
50
51 QueriedCapability()
52 {
53 int device_ordinal = 0;
54 cudaGetDevice(&device_ordinal);
55
56 const int major = component_of(device_ordinal, cudaDevAttrComputeCapabilityMajor);
57 const int minor = component_of(device_ordinal, cudaDevAttrComputeCapabilityMinor);
58
59 encoded = major * 100 + minor * 10;
60 }
61 };
62
63 static const QueriedCapability capability;
64 return capability.encoded;
65}
66
67inline bool frontend_enabled()
68{
69 return device_sm_version() >= 700;
70}
71
72inline bool frontend_verbose()
73{
74 static const bool verbose = env_flag_enabled("OPENNN_CUDNN_FRONTEND_VERBOSE");
75 return verbose;
76}
77
79{
80 return frontend_enabled() && device_sm_version() >= 800;
81}
82
83// Reproducibility runs must reject cuDNN engines whose reductions can change
84// between identical launches. Keep this opt-in because deterministic plans
85// can be slower than the normal heuristic choice.
87{
88 static const bool enabled = env_flag_enabled("OPENNN_DETERMINISTIC", false);
89 return enabled;
90}
91
92// The convolution autotune's second stage. cuDNN's engines for one shape
93// differ in power the way cuBLASLt's tiles do -- occupancy, split-K, tile
94// shape -- and the frontend's autotune ranks them by time alone. With the
95// meter available, every candidate within OPENNN_CONV_ENERGY_TOLERANCE
96// percent of the fastest is run for OPENNN_CONV_ENERGY_WINDOW_MS against the
97// board's energy counter, and the one that costs the least energy per run is
98// taken -- the rule the GEMM tile chooser applies with a modelled power, here
99// with a measured one, because a convolution engine has no tile model to
100// stand on. A candidate must beat the fastest by more than 2% of energy to
101// displace it, so that two readings inside the meter's noise do not flip the
102// choice between runs. 0 percent turns the stage off; so does a machine
103// without NVML.
105{
106 static const bool enabled = env_flag_enabled("OPENNN_CONV_ENERGY_AUTOTUNE", true);
107 return enabled;
108}
109
111{
112 static const float tolerance =
113 float(std::clamp(env_int_or("OPENNN_CONV_ENERGY_TOLERANCE", 10), 0LL, 100LL)) / 100.0f;
114 return tolerance;
115}
116
117inline int64_t conv_energy_window_ms()
118{
119 static const int64_t window = std::clamp(env_int_or("OPENNN_CONV_ENERGY_WINDOW_MS", 300), 100LL, 5000LL);
120 return window;
121}
122
124{
125 static const bool verbose = env_flag_enabled("OPENNN_CONV_ENERGY_VERBOSE");
126 return verbose;
127}
128
134
136{
137 static const bool enabled = env_flag_enabled("OPENNN_GRAPH_TIMING");
138 return enabled;
139}
140
142{
143 static profiler::Stats times;
144 static const bool registered = [] {
145 atexit(+[] {
147 const double total_ms = stats.total_ms();
148 stats.log(
149 format("total_gpu_ms={:.1f}", total_ms),
150 total_ms,
151 "GRAPH_TIMING");
152 });
153 return true;
154 }();
155 (void)registered;
156 return times;
157}
158
159template<typename TensorMap>
160inline void execute_graph(graph::Graph& graph,
161 TensorMap& tensors,
162 void* workspace,
163 const string& what,
164 const string& timing_label)
165{
166 if (timing_label.empty())
167 return check_status(graph.execute(device::get_cudnn_handle(), tensors, workspace), what);
168
169 thread_local device::CudaEvent begin(cudaEventDefault);
170 thread_local device::CudaEvent end(cudaEventDefault);
172
173 check_status(graph.execute(device::get_cudnn_handle(), tensors, workspace), what);
174
177
178 float milliseconds = 0;
179 CHECK_CUDA(cudaEventElapsedTime(&milliseconds, begin.get(), end.get()));
180
181 graph_timing_stats().add(timing_label, milliseconds);
182}
183
184inline void* shared_workspace(int64_t bytes)
185{
186 return bytes > 0 ? ensure_shared_scratch(size_t(bytes)) : nullptr;
187}
188
189template<typename GraphCache, typename Body>
190bool run_frontend(GraphCache& cache, const char* label, Body&& body)
191{
192 const lock_guard lock(cache.access_mutex);
193 if (cache.disabled) return false;
194
195 try
196 {
197 body(cache);
198 return true;
199 }
200 catch (const exception& e)
201 {
202 cache.disabled = true;
203 if(frontend_verbose())
204 logging::warning() << label << ": cudnn-frontend path unavailable ("
205 << e.what() << ").\n";
206 return false;
207 }
208}
209
210[[noreturn]] inline void throw_frontend_unavailable(const string& what)
211{
212 if (!frontend_enabled())
213 throw runtime_error(what + " requires a GPU of compute capability 7.0 or higher.");
214
215 throw runtime_error(
216 what + ": no usable cuDNN plan for this shape. The cudnn-frontend message above "
217 "names the cause; under memory pressure it is normally a failed workspace or "
218 "autotune allocation. Reduce the batch size, or cap the convolution workspace "
219 "with device::set_conv_workspace_cap().");
220}
221
222// Not a switch: the enumeration gains a member with every cuDNN release and
223// -Wswitch-enum would name each one; every type this library does not use is
224// sized as four bytes, which is the largest it allocates for.
225inline int64_t element_bytes(DataType_t dtype)
226{
227 if (dtype == DataType_t::BFLOAT16 || dtype == DataType_t::HALF) return 2;
228 if (dtype == DataType_t::INT8 || dtype == DataType_t::UINT8
229 || dtype == DataType_t::FP8_E4M3 || dtype == DataType_t::FP8_E5M2) return 1;
230 return 4;
231}
232
233inline DataType_t to_dtype(Type t)
234{
235 switch (t)
236 {
237 case Type::Auto:
238 case Type::FP32: return DataType_t::FLOAT;
239 case Type::BF16: return DataType_t::BFLOAT16;
240 case Type::INT8: return DataType_t::INT8;
241 }
242
243 throw invalid_argument("Unsupported data type for a cuDNN graph.");
244}
245
246inline vector<int64_t> nhwc_strides(int64_t c, int64_t h, int64_t w)
247{
248 return {h * w * c, 1, w * c, c};
249}
250
251inline shared_ptr<graph::Graph> new_graph(Type dtype = Type::FP32)
252{
253 auto g = make_shared<graph::Graph>();
254 g->set_io_data_type(to_dtype(dtype))
255 .set_intermediate_data_type(DataType_t::FLOAT)
256 .set_compute_data_type(DataType_t::FLOAT);
257 return g;
258}
259
260inline shared_ptr<graph::Tensor_attributes>
261nhwc_tensor(graph::Graph& graph, const char* name,
262 int64_t n, int64_t c, int64_t h, int64_t w)
263{
264 return graph.tensor(graph::Tensor_attributes()
265 .set_name(name)
266 .set_dim({n, c, h, w})
267 .set_stride(nhwc_strides(c, h, w)));
268}
269
270inline void set_nhwc_output(shared_ptr<graph::Tensor_attributes>& tensor,
271 int64_t n, int64_t c, int64_t h, int64_t w)
272{
273 tensor->set_output(true)
274 .set_dim({n, c, h, w})
275 .set_stride(nhwc_strides(c, h, w));
276}
277
278using VariantPack = unordered_map<shared_ptr<graph::Tensor_attributes>, void*>;
279
280inline vector<int64_t> bhsd_strides(int64_t h, int64_t s, int64_t d, bool interleaved)
281{
282 return interleaved ? vector<int64_t>{s * h * d, d, h * d, 1}
283 : vector<int64_t>{h * s * d, s * d, d, 1};
284}
285
286inline shared_ptr<graph::Tensor_attributes>
287bhsd_tensor(graph::Graph& graph, const char* name,
288 int64_t b, int64_t h, int64_t s, int64_t d, bool interleaved)
289{
290 return graph.tensor(graph::Tensor_attributes()
291 .set_name(name)
292 .set_dim ({b, h, s, d})
293 .set_stride(bhsd_strides(h, s, d, interleaved)));
294}
295
296inline void set_bhsd_output(shared_ptr<graph::Tensor_attributes>& tensor,
297 int64_t b, int64_t h, int64_t s, int64_t d, bool interleaved)
298{
299 tensor->set_output(true)
300 .set_dim ({b, h, s, d})
301 .set_stride(bhsd_strides(h, s, d, interleaved));
302}
303
304inline shared_ptr<graph::Tensor_attributes>
305per_channel_tensor(graph::Graph& graph, const char* name, int64_t channels)
306{
307 return graph.tensor(graph::Tensor_attributes()
308 .set_name(name)
309 .set_data_type(DataType_t::FLOAT)
310 .set_dim({1, channels, 1, 1})
311 .set_stride(nhwc_strides(channels, 1, 1)));
312}
313
314inline void set_per_channel_output(shared_ptr<graph::Tensor_attributes>& tensor, int64_t channels)
315{
316 tensor->set_output(true)
317 .set_data_type(DataType_t::FLOAT)
318 .set_dim({1, channels, 1, 1})
319 .set_stride(nhwc_strides(channels, 1, 1));
320}
321
322template <typename... Args>
323inline string timing_label(format_string<Args...> fmt, Args&&... args)
324{
325 if (!graph_timing_enabled()) return {};
326 return format(fmt, std::forward<Args>(args)...);
327}
328
329inline shared_ptr<graph::Tensor_attributes>
330scalar_tensor(graph::Graph& graph, const char* name, DataType_t dtype,
331 bool pass_by_value = false, int64_t batch = 1)
332{
333 return graph.tensor(graph::Tensor_attributes()
334 .set_name(name)
335 .set_dim ({batch, 1, 1, 1})
336 .set_stride({1, 1, 1, 1})
337 .set_data_type(dtype)
338 .set_is_pass_by_value(pass_by_value));
339}
340
341inline int64_t candidate_limit_from(long long value)
342{
343 return value <= 0 ? numeric_limits<int64_t>::max() : int64_t(value);
344}
345
347{
348 static const int64_t limit = candidate_limit_from(env_int_or("OPENNN_AUTOTUNE_CANDIDATES", 8));
349 return limit;
350}
351
352inline int64_t autotune_candidate_limit(const string& tag)
353{
354 static const auto per_kind = []
355 {
356 map<string, int64_t> limits;
357 for (const char* kind : {"forward", "wgrad", "dgrad"})
358 {
359 string name = "OPENNN_AUTOTUNE_CANDIDATES_" + string(kind);
360 for (char& c : name) c = char(toupper(c));
361 if (const char* setting = getenv(name.c_str()); setting && *setting)
362 limits[kind] = candidate_limit_from(env_int_or(name.c_str(), 0));
363 }
364 return limits;
365 }();
366
367 const auto found = per_kind.find(tag);
368 return found == per_kind.end() ? autotune_candidate_limit() : found->second;
369}
370
371inline vector<HeurMode_t> heuristic_modes()
372{
373 static const vector<HeurMode_t> modes = []
374 {
375 const char* setting = getenv("OPENNN_CUDNN_HEURISTICS");
376 const string value = setting ? setting : "A";
377 if (value == "B") return vector<HeurMode_t>{HeurMode_t::B, HeurMode_t::FALLBACK};
378 if (value == "AB") return vector<HeurMode_t>{HeurMode_t::A, HeurMode_t::B, HeurMode_t::FALLBACK};
379 return vector<HeurMode_t>{HeurMode_t::A, HeurMode_t::FALLBACK};
380 }();
381 return modes;
382}
383
384inline const vector<NumericalNote_t>& conv_engine_notes()
385{
386 static const vector<NumericalNote_t> notes = []
387 {
388 vector<NumericalNote_t> selected;
389 const char* setting = getenv("OPENNN_CONV_ENGINE_NOTES");
390 const string value = setting ? setting : "";
391 if (value.find("WINOGRAD") != string::npos) selected.push_back(NumericalNote_t::WINOGRAD);
392 if (value.find("FFT") != string::npos) selected.push_back(NumericalNote_t::FFT);
393 return selected;
394 }();
395 return notes;
396}
397
398inline bool build_top_candidates(graph::Graph& graph, int64_t limit)
399{
400 const int64_t count = graph.get_execution_plan_count();
401 int64_t built = 0;
402
403 for (int64_t index = 0; index < count && built < limit; ++index)
404 if (graph.build_plan_at_index(index).is_good()) ++built;
405
406 return built > 0;
407}
408
410{
411 static const bool enabled = env_flag_enabled("OPENNN_CUDNN_PLAN_CACHE", true);
412 return enabled;
413}
414
415inline bool sdpa_autotune_enabled();
416inline int64_t sdpa_workspace_cap_override_bytes();
417
418inline const std::filesystem::path& plan_cache_directory()
419{
420 static const std::filesystem::path directory = []
421 {
422 const char* override_path = getenv("OPENNN_CUDNN_PLAN_CACHE_DIR");
423
424 std::filesystem::path root;
425
426 if (override_path && *override_path)
427 root = std::filesystem::path(override_path);
428 else
429 {
430 std::error_code error;
431 const std::filesystem::path temporary = std::filesystem::temp_directory_path(error);
432 if (error) return std::filesystem::path{};
433 root = temporary / "opennn-cudnn-plans";
434 }
435
436 return root / format("sm{}-cudnn{}", device_sm_version(), CUDNN_VERSION);
437 }();
438
439 return directory;
440}
441
442inline std::filesystem::path plan_cache_file(const graph::Graph& graph)
443{
444 // The cache key is the serialised graph, and serialising asks cuDNN for
445 // CUDNN_ATTR_EXECUTION_PLAN_JSON_REPRESENTATION -- an attribute the
446 // Windows builds of cuDNN 9 do not implement. store_cached_plan() already
447 // treats a failed serialise as "do not cache"; this path did not, so the
448 // throw escaped through load_cached_plan(), the frontend disabled itself,
449 // and every convolution became fatal. A machine that cannot serialise a
450 // plan should lose the cache, not the GPU.
451 json structure;
452 try
453 {
454 graph.serialize(structure);
455 }
456 catch (const std::exception&)
457 {
458 static std::once_flag reported;
459 std::call_once(reported, []{
460 logging::warning() << "cudnn plan cache: this cuDNN build cannot serialise "
461 "execution plans; continuing without the cache.\n"; });
462 return {};
463 }
464 structure.erase("gid");
465
466 size_t selection = size_t(autotune_candidate_limit());
467 for (const char* kind : {"forward", "wgrad", "dgrad"})
468 selection = selection * 31 + size_t(autotune_candidate_limit(kind));
469 for (const HeurMode_t mode : heuristic_modes()) selection = selection * 31 + size_t(mode) + 1;
470 for (const NumericalNote_t note : conv_engine_notes()) selection = selection * 31 + size_t(note) + 7;
471 selection = selection * 31 + (deterministic_algorithms_enabled() ? 11u : 0u);
472
473 selection = selection * 31 + (sdpa_autotune_enabled() ? 3u : 0u);
474
475 // The cached entry is the plan that won an autotune, and the SDPA workspace
476 // cap changes which plan that is, so a capped build must not load an
477 // uncapped winner from the same directory. Only the override belongs here:
478 // the shape half of the cap is a function of the extents, which the
479 // serialised graph already carries.
480 selection = selection * 31 + size_t(sdpa_workspace_cap_override_bytes() + 2);
481
482 // Whether the attention plan was autotuned belongs in the key for the
483 // same reason the workspace cap does: a heuristic plan stored by a run
484 // with the autotune off must not be handed to a run that asked for the
485 // measured one, or the knob measures nothing once the cache is warm.
486 const size_t key = std::hash<json>{}(structure)
487 ^ (std::hash<int64_t>{}(device::conv_workspace_limit_bytes()) << 1)
488 ^ (std::hash<bool>{}(device::conv_autotune_enabled()) << 2)
489 ^ (std::hash<size_t>{}(selection) << 3)
490 ^ (std::hash<bool>{}(sdpa_autotune_enabled()) << 4)
491 ^ (std::hash<bool>{}(device::allow_tf32()) << 6)
492 ^ (std::hash<size_t>{}(conv_energy_stage_active()
493 ? size_t(1000 + int(conv_energy_tolerance() * 100.0f + 0.5f))
494 : size_t(0)) << 5);
495
496 return plan_cache_directory() / format("{:016x}.plan", key);
497}
498
499inline bool load_cached_plan(graph::Graph& graph, const cudnnHandle_t handle,
500 int64_t& workspace_bytes)
501{
502 if (!plan_cache_enabled() || plan_cache_directory().empty()) return false;
503
504 std::error_code failed;
505 const std::filesystem::path file = plan_cache_file(graph);
506 if (file.empty()) return false;
507 if (!std::filesystem::exists(file, failed) || failed) return false;
508
509 std::ifstream stream(file, std::ios::binary);
510 if (!stream) return false;
511
512 const vector<uint8_t> blob((std::istreambuf_iterator<char>(stream)),
513 std::istreambuf_iterator<char>());
514 if (blob.empty()) return false;
515
516 constexpr bool enforce_precompiled = false;
517 constexpr bool run_warmup = false;
518
519 if (graph.deserialize(handle, blob, enforce_precompiled, run_warmup).is_bad()) return false;
520
521 return graph.get_workspace_size(workspace_bytes).is_good();
522}
523
524inline void store_cached_plan(graph::Graph& graph)
525{
526 if (!plan_cache_enabled() || plan_cache_directory().empty()) return;
527
528 vector<uint8_t> blob;
529
530 // Status-checking this is not enough: cudnn-frontend throws out of
531 // serialize() rather than returning when the backend refuses the JSON
532 // attribute, which is what Windows cuDNN does. The status check stays for
533 // the builds that report it politely.
534 try
535 {
536 if (graph.serialize(blob, false).is_bad() || blob.empty()) return;
537 }
538 catch (const std::exception&)
539 {
540 return;
541 }
542
543 std::error_code failed;
544 std::filesystem::create_directories(plan_cache_directory(), failed);
545 if (failed) return;
546
547 static atomic<uint64_t> sequence{0};
548
549 const std::filesystem::path file = plan_cache_file(graph);
550 if (file.empty()) return;
551 const std::filesystem::path pending = file.string()
552 + format(".{:x}-{}.tmp", std::hash<thread::id>{}(this_thread::get_id()), sequence++);
553
554 {
555 std::ofstream stream(pending, std::ios::binary | std::ios::trunc);
556 if (!stream) return;
557 stream.write(reinterpret_cast<const char*>(blob.data()), std::streamsize(blob.size()));
558 if (!stream) { std::filesystem::remove(pending, failed); return; }
559 }
560
561 std::filesystem::rename(pending, file, failed);
562 if (failed) std::filesystem::remove(pending, failed);
563}
564
565inline int64_t autotune_workspace_bytes(const graph::Graph& graph)
566{
567 int64_t maximum = 0;
568 const int64_t count = graph.get_execution_plan_count();
569
570 for (int64_t index = 0; index < count; ++index)
571 {
572 int64_t bytes = 0;
573 if (graph.get_workspace_size_plan_at_index(index, bytes).is_good())
574 maximum = max(maximum, bytes);
575 }
576
577 return maximum;
578}
579
580// On by default since the transformer cells were re-measured with it
581// (2026-09-06, RTX 5070 Ti, cuDNN 9.25.1): timing cuDNN's attention engines
582// under the workspace cap below instead of taking heuristic A's first pick
583// read 5,413 against 5,335 sequences/s on inference and 1,353 against 1,330
584// on training, repeatable to the unit on launches that otherwise do not move.
585// OPENNN_SDPA_AUTOTUNE=0 restores the heuristic plan.
587{
588 static const bool enabled = env_flag_enabled("OPENNN_SDPA_AUTOTUNE", true);
589 return enabled;
590}
591
592// Megabytes, mirroring OPENNN_CONV_WORKSPACE_MB: 0 removes the cap and restores
593// pick-by-time over every candidate, a positive value pins it, and unset leaves
594// the shape-derived bound below. It exists so the cap stays measurable rather
595// than baked in -- it is the knob a sweep varies now that the autotune is on
596// by default (see sdpa_autotune_enabled()).
598{
599 static const int64_t bytes = []
600 {
601 const long long megabytes = env_int_or("OPENNN_SDPA_WORKSPACE_MB", -1);
602 return megabytes < 0 ? int64_t(-1) : int64_t(megabytes) * 1024 * 1024;
603 }();
604 return bytes;
605}
606
607// A plan may not ask for more scratch than an fp32 copy of the node's own
608// Q, K and V. That is the same rule the convolution path applies -- bound the
609// workspace by the tensors the layer already pays for -- but it has to be
610// re-derived here rather than reused, because
611// device::conv_workspace_limit_bytes() is the largest *convolution* activation
612// slot (forward_propagation.cpp) and the cnn benchmark pins it flat at 16 MiB.
613// Neither number is a statement about an attention node, and 16 MiB would bar
614// every plan the transformer cell needs.
615//
616// The bound is exactly the size of the fp32 dQ/dK/dV accumulators a flash
617// engine legitimately keeps, so it excludes no flash plan, while an engine that
618// materialises the B*H*Sq*Skv score matrix in fp32 is admitted for at most one
619// such buffer and excluded outright once Sq passes 3*head_dim, where that
620// matrix overtakes the accumulators and then keeps growing quadratically. At
621// the transformer cell's 32 x 8 x 130 x 64 that is a 24.5 MiB cap against a
622// 16.5 MiB score matrix and 12.2 MiB of bf16 Q/K/V.
623inline int64_t sdpa_workspace_cap_bytes(int64_t batch, int64_t heads,
624 int64_t q_seq, int64_t src_seq, int64_t head_dim)
625{
626 if (const int64_t override_bytes = sdpa_workspace_cap_override_bytes(); override_bytes >= 0)
627 return override_bytes;
628
629 const int64_t operands = 4 * batch * heads * head_dim * (q_seq + 2 * src_seq);
630 const int64_t statistics = 4 * batch * heads * q_seq;
631
632 // The floor keeps a small shape -- a unit test, a short prompt -- from being
633 // capped below a plan's fixed scratch (tile counters, semaphores, the
634 // frontend's own node workspace), where every candidate is small anyway and
635 // there is nothing to exclude.
636 return max(operands + statistics, int64_t(4) * 1024 * 1024);
637}
638
639inline bool finalize_attention(graph::Graph& graph, const string& tag, int64_t& workspace_bytes,
640 bool allow_autotune = false, int64_t workspace_cap = 0)
641{
642 const cudnnHandle_t handle = device::get_cudnn_handle();
643
644 check_status(graph.validate(), tag + " validate");
645
646 if (load_cached_plan(graph, handle, workspace_bytes)) return false;
647
648 check_status(graph.build_operation_graph(handle), tag + " build_operation_graph");
649
650 if (allow_autotune && sdpa_autotune_enabled())
651 {
652 check_status(graph.create_execution_plans({HeurMode_t::A, HeurMode_t::B, HeurMode_t::FALLBACK}),
653 tag + " create_execution_plans");
654
655 if (workspace_cap > 0)
656 graph.deselect_workspace_greater_than(workspace_cap);
657
659 {
660 // What the slot carries is the workspace the graph would execute
661 // with, which is the candidate plan's; autotune_now() overwrites it
662 // with the winner's and sizes its own tuning scratch from
663 // autotune_workspace_bytes(). Parking that max-over-candidates here
664 // instead left the slot's persistent size at the largest built
665 // candidate's, and that is the size shared_workspace() grows to on
666 // any path where the tuning run does not happen.
667 check_status(graph.get_workspace_size(workspace_bytes), tag + " workspace");
668 return true;
669 }
670
671 // Nothing survived the cap, so the heuristic plan below has to be chosen
672 // without it. The cap narrows an autotune; it must never be the reason
673 // attention is left with no plan at all.
674 graph.deselect_workspace_greater_than(numeric_limits<int64_t>::max());
675 }
676
677 check_status(graph.create_execution_plans({HeurMode_t::A}), tag + " create_execution_plans");
678 check_status(graph.build_plans(handle, BuildPlanPolicy_t::HEURISTICS_CHOICE), tag + " build_plans");
679 check_status(graph.get_workspace_size(workspace_bytes), tag + " workspace");
680
681 store_cached_plan(graph);
682 return false;
683}
684
685inline shared_ptr<graph::Tensor_attributes>
686seq_len_scalar(graph::Graph& graph, const char* name, int64_t batch = 1)
687{
688 return scalar_tensor(graph, name, DataType_t::INT32, false, batch);
689}
690
691// fp32_graph: the graph's operands are fp32, so its tensor-core engines would
692// run TF32; when TF32 is disallowed those engines are deselected and the plan
693// is IEEE fp32 (see device::allow_tf32()).
694inline bool finalize(graph::Graph& graph, int64_t& workspace_bytes, const string& tag,
695 const bool fp32_graph = false)
696{
697 const cudnnHandle_t handle = device::get_cudnn_handle();
698 const bool request_autotune = device::conv_autotune_enabled();
699 const bool ieee_fp32 = fp32_graph && !device::allow_tf32();
700
701 workspace_bytes = 0;
702
703 check_status(graph.validate(), tag + " validate");
704
705 if (load_cached_plan(graph, handle, workspace_bytes)) return false;
706
707 check_status(graph.build_operation_graph(handle), tag + " build_operation_graph");
708 const int64_t conv_workspace_cap = device::conv_workspace_limit_bytes();
709
710 const bool convolution_graph = tag == "forward" || tag == "wgrad" || tag == "dgrad";
711 const auto prepare_candidates = [&](bool restrict_notes)
712 {
713 check_status(graph.create_execution_plans(heuristic_modes()), tag + " create_execution_plans");
714 if (conv_workspace_cap > 0)
715 graph.deselect_workspace_greater_than(conv_workspace_cap);
716 // IEEE fp32 means no tensor-core (TF32) engine, no engine that
717 // down-converts its inputs, and no engine that reduces in a narrower
718 // type -- the three notes cuDNN uses to say "not the arithmetic the
719 // operands are declared in".
720 if (ieee_fp32)
721 graph.deselect_numeric_notes({NumericalNote_t::TENSOR_CORE,
722 NumericalNote_t::DOWN_CONVERT_INPUTS,
723 NumericalNote_t::REDUCED_PRECISION_REDUCTION});
725 graph.deselect_numeric_notes({NumericalNote_t::NONDETERMINISTIC});
726 if (restrict_notes)
727 graph.select_numeric_notes(conv_engine_notes());
728 };
729
730 const auto build_candidates = [&]() -> bool
731 {
732 if (request_autotune)
734 return graph.build_plans(handle, BuildPlanPolicy_t::HEURISTICS_CHOICE).is_good();
735 };
736
737 bool restricted = convolution_graph && !conv_engine_notes().empty();
738 prepare_candidates(restricted);
739 bool built = build_candidates();
740 if (!built && restricted)
741 {
742 restricted = false;
743 prepare_candidates(false);
744 built = build_candidates();
745 }
746 if (!built)
747 check_status(graph.build_plans(handle, BuildPlanPolicy_t::HEURISTICS_CHOICE), tag + " build_plans");
748
749 if (request_autotune && built) return true;
750
751 check_status(graph.get_workspace_size(workspace_bytes), tag + " get_workspace_size");
752
753 store_cached_plan(graph);
754
755 return false;
756}
757
758inline void report_autotune_skipped(const char* tag, const char* reason)
759{
761 logging::warning() << (tag ? tag : "autotune")
762 << ": autotune skipped, keeping the heuristic plan (" << reason << ").\n";
763}
764
765// run_slot() labels its calls "ConvolutionOperator fwd", "... wgrad",
766// "... dgrad", "... bgrad" and "... fwd folded"; attention's are "sdpa fwd"
767// and "sdpa bwd". The stage applies to the convolution family only.
768inline bool is_convolution_tag(const char* tag)
769{
770 return tag && std::string_view(tag).find("ConvolutionOperator") != std::string_view::npos;
771}
772
773// Runs after graph.autotune() has ranked the built plans by time, winner at
774// index 0. Re-times each plan briefly (the frontend keeps its timings
775// private), meters the energy of every plan inside the tolerance, and makes
776// the cheapest of them the candidate. build_plan_at_index() on an already
777// built plan is the frontend's way of selecting it: it skips the build and
778// sets the candidate, which is what execute() and serialize() then use.
779template<typename TensorMap>
780inline void select_plan_by_energy(graph::Graph& graph, TensorMap& tensors, void* workspace, const char* tag)
781{
782 const int64_t count = graph.get_execution_plan_count();
783 if (count < 2) return;
784
785 const cudnnHandle_t handle = device::get_cudnn_handle();
786 cudaStream_t stream = nullptr;
787 cudnnGetStream(handle, &stream);
788 cudaEvent_t start = nullptr, stop = nullptr;
789 if (cudaEventCreate(&start) != cudaSuccess || cudaEventCreate(&stop) != cudaSuccess) return;
790
791 struct Reading { int64_t index; float microseconds; double millijoules; };
792 vector<Reading> timed;
793
794 // Time: the median of five back-to-back runs, after one warm-up, on the
795 // same stream the plan will execute on.
796 for (int64_t index = 0; index < count; ++index)
797 {
798 if (graph.execute_plan_at_index(handle, tensors, workspace, index).is_bad()) continue;
799 float samples[5] = {}; // microseconds
800 bool ok = true;
801 for (float& sample : samples)
802 {
803 cudaEventRecord(start, stream);
804 ok = graph.execute_plan_at_index(handle, tensors, workspace, index).is_good();
805 cudaEventRecord(stop, stream);
806 cudaEventSynchronize(stop);
807 if (!ok) break;
808 float milliseconds = 0.0f;
809 cudaEventElapsedTime(&milliseconds, start, stop);
810 sample = milliseconds * 1000.0f;
811 }
812 if (!ok) continue;
813 std::sort(samples, samples + 5);
814 timed.push_back({index, samples[2], 0.0});
815 }
816 if (timed.size() < 2) { cudaEventDestroy(start); cudaEventDestroy(stop); device::reset_last_error(); return; }
817
818 const auto fastest = std::min_element(timed.begin(), timed.end(),
819 [](const Reading& a, const Reading& b) { return a.microseconds < b.microseconds; });
820 const float budget = fastest->microseconds * (1.0f + conv_energy_tolerance());
821
822 // Energy: each admissible plan runs back to back for the window while
823 // the driver's power ring samples the board every ~20 ms; the mean of
824 // the samples inside the window times the window is the energy, divided
825 // by the runs. A 300 ms window holds about fifteen samples, enough to
826 // put the mean well inside the 2% margin below; a window that turns out
827 // to hold fewer than five is not trusted and leaves the time-ranked
828 // choice alone.
829 vector<Reading*> admissible;
830 for (Reading& reading : timed)
831 if (reading.microseconds <= budget) admissible.push_back(&reading);
832 if (admissible.size() < 2) { cudaEventDestroy(start); cudaEventDestroy(stop); device::reset_last_error(); return; }
833
834 for (Reading* reading : admissible)
835 {
836 const int64_t runs = std::max<int64_t>(10, int64_t(double(conv_energy_window_ms()) * 1000.0
837 / double(std::max(reading->microseconds, 1.0f))));
838 (void)graph.execute_plan_at_index(handle, tensors, workspace, reading->index);
839 cudaStreamSynchronize(stream);
840 const unsigned long long window_begin = device::energy_meter_begin();
841 cudaEventRecord(start, stream);
842 for (int64_t run = 0; run < runs; ++run)
843 (void)graph.execute_plan_at_index(handle, tensors, workspace, reading->index);
844 cudaEventRecord(stop, stream);
845 cudaEventSynchronize(stop);
846 float milliseconds = 0.0f;
847 cudaEventElapsedTime(&milliseconds, start, stop);
848 int samples = 0;
849 const double watts = device::energy_meter_window_watts(window_begin, samples);
850 reading->millijoules = samples >= 5 && watts > 0.0
851 ? watts * double(milliseconds) / double(runs) // W * ms = mJ
852 : 0.0;
853 // The window's own timing is the better time figure -- many runs, not
854 // five -- so it replaces the short one for the decision below.
855 reading->microseconds = milliseconds * 1000.0f / float(runs);
856 }
857 cudaEventDestroy(start);
858 cudaEventDestroy(stop);
860
861 Reading* baseline = nullptr;
862 for (Reading* reading : admissible)
863 if (reading->millijoules > 0.0 && (!baseline || reading->microseconds < baseline->microseconds))
864 baseline = reading;
865 if (!baseline) return;
866
867 Reading* chosen = baseline;
868 for (Reading* reading : admissible)
869 if (reading->millijoules > 0.0 && reading->millijoules < chosen->millijoules * 0.98
870 && reading->microseconds <= baseline->microseconds * (1.0f + conv_energy_tolerance()))
871 chosen = reading;
872
874 {
875 string chosen_name, fastest_name;
876 (void)graph.get_plan_name_at_index(chosen->index, chosen_name);
877 (void)graph.get_plan_name_at_index(baseline->index, fastest_name);
878 auto message = logging::info();
879 message << (tag ? tag : "conv") << ": energy autotune over " << admissible.size() << " of "
880 << timed.size() << " plans: "
881 << (chosen == baseline ? "kept the fastest " : "chose ") << chosen_name
882 << " (" << chosen->microseconds << " us, " << chosen->millijoules << " mJ)";
883 if (chosen != baseline)
884 message << " over " << fastest_name << " (" << baseline->microseconds << " us, "
885 << baseline->millijoules << " mJ)";
886 message << "\n";
887 }
888
889 if (chosen != baseline)
890 {
891 const auto selected = graph.build_plan_at_index(handle, chosen->index);
892 (void)selected;
893 }
894}
895
896template<typename TensorMap>
897inline void autotune_now(bool& pending, graph::Graph& graph,
898 TensorMap& tensors, int64_t& workspace_bytes,
899 const char* tag = nullptr)
900{
901 if (!pending) return;
902 pending = false;
904
905 Buffer tune_workspace{Device::CUDA};
906 try
907 {
908 const int64_t tune_bytes = autotune_workspace_bytes(graph);
909 if (tune_bytes > 0) tune_workspace.resize_bytes(Index(tune_bytes), Device::CUDA);
910 check_status(graph.autotune(device::get_cudnn_handle(), tensors, tune_workspace.data()), "autotune");
911
913 select_plan_by_energy(graph, tensors, tune_workspace.data(), tag);
914
915 store_cached_plan(graph);
916 }
917 catch (const exception& e)
918 {
919 report_autotune_skipped(tag, e.what());
920 }
921 catch (...)
922 {
923 report_autotune_skipped(tag, "unknown error");
924 }
925
927
928 workspace_bytes = graph.get_workspace_size();
929}
930
931template<typename TensorMap>
932inline void autotune_with_scratch(bool& pending, graph::Graph& graph,
933 const TensorMap& tensors, int64_t& workspace_bytes,
934 const char* tag = nullptr)
935{
936 if (!pending) return;
938
939 TensorMap scratch = tensors;
940 vector<Buffer> buffers;
941 buffers.reserve(scratch.size());
942
943 try
944 {
945 for (auto& [tensor, pointer] : scratch)
946 {
947 if (tensor->get_is_pass_by_value()) continue;
948
949 int64_t elements = 1;
950 for (const int64_t dimension : tensor->get_dim()) elements *= dimension;
951
952 Buffer& buffer = buffers.emplace_back(Device::CUDA);
953 buffer.resize_bytes(Index(elements * element_bytes(tensor->get_data_type())),
955 pointer = buffer.data();
956 }
957 }
958 catch (const exception& e)
959 {
960 pending = false;
961 buffers.clear();
963 report_autotune_skipped(tag, e.what());
964 workspace_bytes = graph.get_workspace_size();
965 return;
966 }
967
968 autotune_now(pending, graph, scratch, workspace_bytes, tag);
969}
970
972{
973 shared_ptr<graph::Graph> graph;
974 int64_t workspace_bytes = 0;
975 bool autotune_pending = false;
976
977 explicit operator bool() const noexcept { return graph != nullptr; }
978 graph::Graph& operator*() const noexcept { return *graph; }
979
980 void build(shared_ptr<graph::Graph> built, const string& tag, const bool fp32_graph = false)
981 {
982 graph.reset();
983 autotune_pending = finalize(*built, workspace_bytes, tag, fp32_graph);
984 graph = std::move(built);
985 }
986
987 // Attention finalizes under its own policy -- its own autotune switch, its
988 // own heuristic modes and its own workspace cap -- so it cannot share
989 // build()'s convolution one. Keeping it a named second entry point makes
990 // that difference visible instead of hiding it behind a slot that looks
991 // interchangeable. The cap comes from the caller because it is a function
992 // of the node's extents, and the graph exposes those only through a full
993 // serialise, not an accessor; sdpa_workspace_cap_bytes() derives it from
994 // the cache key instead, and 0 leaves the candidates uncapped.
995 void build_attention(shared_ptr<graph::Graph> built, const string& tag,
996 bool allow_autotune = true, int64_t workspace_cap = 0)
997 {
998 PROFILE_SCOPE_HOST("cudnn:build " + tag);
999
1000 graph.reset();
1002 allow_autotune, workspace_cap);
1003 graph = std::move(built);
1004 }
1005};
1006
1007template<typename TensorMap>
1008inline void run_slot(GraphSlot& slot, TensorMap& tensors, const char* what,
1009 const string& timing_label, bool scratch_autotune)
1010{
1011 if (scratch_autotune)
1012 autotune_with_scratch(slot.autotune_pending, *slot.graph, tensors, slot.workspace_bytes, what);
1013 else
1014 autotune_now(slot.autotune_pending, *slot.graph, tensors, slot.workspace_bytes, what);
1015 execute_graph(*slot.graph, tensors, shared_workspace(slot.workspace_bytes), what, timing_label);
1016}
1017
1018}
1019
1020#endif
Definition device_backend.h:298
DeviceEvent get() const noexcept
Definition device_backend.h:314
Definition profiler.h:43
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
double total_ms() const
Definition profiler.h:70
Definition cudnn_frontend_utilities.h:23
shared_ptr< graph::Tensor_attributes > seq_len_scalar(graph::Graph &graph, const char *name, int64_t batch=1)
Definition cudnn_frontend_utilities.h:686
void select_plan_by_energy(graph::Graph &graph, TensorMap &tensors, void *workspace, const char *tag)
Definition cudnn_frontend_utilities.h:780
vector< int64_t > bhsd_strides(int64_t h, int64_t s, int64_t d, bool interleaved)
Definition cudnn_frontend_utilities.h:280
void run_slot(GraphSlot &slot, TensorMap &tensors, const char *what, const string &timing_label, bool scratch_autotune)
Definition cudnn_frontend_utilities.h:1008
bool deterministic_algorithms_enabled()
Definition cudnn_frontend_utilities.h:86
shared_ptr< graph::Graph > new_graph(Type dtype=Type::FP32)
Definition cudnn_frontend_utilities.h:251
int64_t candidate_limit_from(long long value)
Definition cudnn_frontend_utilities.h:341
void set_per_channel_output(shared_ptr< graph::Tensor_attributes > &tensor, int64_t channels)
Definition cudnn_frontend_utilities.h:314
bool conv_energy_verbose()
Definition cudnn_frontend_utilities.h:123
void autotune_with_scratch(bool &pending, graph::Graph &graph, const TensorMap &tensors, int64_t &workspace_bytes, const char *tag=nullptr)
Definition cudnn_frontend_utilities.h:932
shared_ptr< graph::Tensor_attributes > bhsd_tensor(graph::Graph &graph, const char *name, int64_t b, int64_t h, int64_t s, int64_t d, bool interleaved)
Definition cudnn_frontend_utilities.h:287
const auto check_status
Definition cudnn_frontend_utilities.h:28
bool plan_cache_enabled()
Definition cudnn_frontend_utilities.h:409
const std::filesystem::path & plan_cache_directory()
Definition cudnn_frontend_utilities.h:418
int64_t conv_energy_window_ms()
Definition cudnn_frontend_utilities.h:117
int64_t autotune_candidate_limit()
Definition cudnn_frontend_utilities.h:346
bool build_top_candidates(graph::Graph &graph, int64_t limit)
Definition cudnn_frontend_utilities.h:398
int64_t sdpa_workspace_cap_override_bytes()
Definition cudnn_frontend_utilities.h:597
void report_autotune_skipped(const char *tag, const char *reason)
Definition cudnn_frontend_utilities.h:758
bool conv_energy_autotune_enabled()
Definition cudnn_frontend_utilities.h:104
bool finalize(graph::Graph &graph, int64_t &workspace_bytes, const string &tag, const bool fp32_graph=false)
Definition cudnn_frontend_utilities.h:694
bool load_cached_plan(graph::Graph &graph, const cudnnHandle_t handle, int64_t &workspace_bytes)
Definition cudnn_frontend_utilities.h:499
void set_nhwc_output(shared_ptr< graph::Tensor_attributes > &tensor, int64_t n, int64_t c, int64_t h, int64_t w)
Definition cudnn_frontend_utilities.h:270
vector< HeurMode_t > heuristic_modes()
Definition cudnn_frontend_utilities.h:371
bool conv_energy_stage_active()
Definition cudnn_frontend_utilities.h:129
const vector< NumericalNote_t > & conv_engine_notes()
Definition cudnn_frontend_utilities.h:384
int64_t element_bytes(DataType_t dtype)
Definition cudnn_frontend_utilities.h:225
shared_ptr< graph::Tensor_attributes > nhwc_tensor(graph::Graph &graph, const char *name, int64_t n, int64_t c, int64_t h, int64_t w)
Definition cudnn_frontend_utilities.h:261
void autotune_now(bool &pending, graph::Graph &graph, TensorMap &tensors, int64_t &workspace_bytes, const char *tag=nullptr)
Definition cudnn_frontend_utilities.h:897
bool bn_frontend_enabled()
Definition cudnn_frontend_utilities.h:78
shared_ptr< graph::Tensor_attributes > per_channel_tensor(graph::Graph &graph, const char *name, int64_t channels)
Definition cudnn_frontend_utilities.h:305
void store_cached_plan(graph::Graph &graph)
Definition cudnn_frontend_utilities.h:524
string timing_label(format_string< Args... > fmt, Args &&... args)
Definition cudnn_frontend_utilities.h:323
void throw_frontend_unavailable(const string &what)
Definition cudnn_frontend_utilities.h:210
int64_t autotune_workspace_bytes(const graph::Graph &graph)
Definition cudnn_frontend_utilities.h:565
std::filesystem::path plan_cache_file(const graph::Graph &graph)
Definition cudnn_frontend_utilities.h:442
profiler::Stats & graph_timing_stats()
Definition cudnn_frontend_utilities.h:141
void execute_graph(graph::Graph &graph, TensorMap &tensors, void *workspace, const string &what, const string &timing_label)
Definition cudnn_frontend_utilities.h:160
bool run_frontend(GraphCache &cache, const char *label, Body &&body)
Definition cudnn_frontend_utilities.h:190
bool frontend_enabled()
Definition cudnn_frontend_utilities.h:67
vector< int64_t > nhwc_strides(int64_t c, int64_t h, int64_t w)
Definition cudnn_frontend_utilities.h:246
bool frontend_verbose()
Definition cudnn_frontend_utilities.h:72
DataType_t to_dtype(Type t)
Definition cudnn_frontend_utilities.h:233
shared_ptr< graph::Tensor_attributes > scalar_tensor(graph::Graph &graph, const char *name, DataType_t dtype, bool pass_by_value=false, int64_t batch=1)
Definition cudnn_frontend_utilities.h:330
unordered_map< shared_ptr< graph::Tensor_attributes >, void * > VariantPack
Definition cudnn_frontend_utilities.h:278
bool graph_timing_enabled()
Definition cudnn_frontend_utilities.h:135
float conv_energy_tolerance()
Definition cudnn_frontend_utilities.h:110
void set_bhsd_output(shared_ptr< graph::Tensor_attributes > &tensor, int64_t b, int64_t h, int64_t s, int64_t d, bool interleaved)
Definition cudnn_frontend_utilities.h:296
int device_sm_version()
Definition cudnn_frontend_utilities.h:33
bool finalize_attention(graph::Graph &graph, const string &tag, int64_t &workspace_bytes, bool allow_autotune=false, int64_t workspace_cap=0)
Definition cudnn_frontend_utilities.h:639
bool is_convolution_tag(const char *tag)
Definition cudnn_frontend_utilities.h:768
bool sdpa_autotune_enabled()
Definition cudnn_frontend_utilities.h:586
void * shared_workspace(int64_t bytes)
Definition cudnn_frontend_utilities.h:184
constexpr size_t graph_cache_capacity
Definition cudnn_frontend_utilities.h:26
int64_t sdpa_workspace_cap_bytes(int64_t batch, int64_t heads, int64_t q_seq, int64_t src_seq, int64_t head_dim)
Definition cudnn_frontend_utilities.h:623
bool conv_autotune_enabled() noexcept
void synchronize_event(DeviceEvent)
bool energy_meter_available() noexcept
bool allow_tf32() noexcept
DnnHandle get_cudnn_handle()
DeviceStream get_compute_stream()
void record_event(DeviceEvent, DeviceStream)
unsigned long long energy_meter_begin() noexcept
double energy_meter_window_watts(unsigned long long begin, int &samples) noexcept
void reset_last_error() noexcept
int64_t conv_workspace_limit_bytes() noexcept
int lanes_available() noexcept
void synchronize(DeviceStream=nullptr)
Line info()
Definition log.h:77
Line warning()
Definition log.h:76
float maximum(const MatrixR &)
void * ensure_shared_scratch(size_t bytes)
Definition device_backend.h:382
@ CUDA
Definition configuration.h:12
bool env_flag_enabled(const char *) noexcept
Type
Definition configuration.h:13
@ Auto
Definition configuration.h:13
@ FP32
Definition configuration.h:13
@ INT8
Definition configuration.h:13
@ BF16
Definition configuration.h:13
long long env_int_or(const char *, long long default_value) noexcept
#define throw_if(condition,...)
Definition opennn_types.h:243
#define CHECK_CUDA(x)
Definition opennn_types.h:85
#define PROFILE_SCOPE_HOST(name)
Definition profiler.h:289
Definition tensor_types.h:340
void resize_bytes(Index byte_count, Device new_device)
Definition tensor_types.h:410
void * data() const noexcept
Definition tensor_types.h:383
Definition cudnn_frontend_utilities.h:972
bool autotune_pending
Definition cudnn_frontend_utilities.h:975
void build_attention(shared_ptr< graph::Graph > built, const string &tag, bool allow_autotune=true, int64_t workspace_cap=0)
Definition cudnn_frontend_utilities.h:995
void build(shared_ptr< graph::Graph > built, const string &tag, const bool fp32_graph=false)
Definition cudnn_frontend_utilities.h:980
int64_t workspace_bytes
Definition cudnn_frontend_utilities.h:974
shared_ptr< graph::Graph > graph
Definition cudnn_frontend_utilities.h:973
graph::Graph & operator*() const noexcept
Definition cudnn_frontend_utilities.h:978