Skip to main content

ort/ep/
cuda.rs

1use alloc::string::ToString;
2use core::ops::BitOr;
3
4use super::{ArenaExtendStrategy, ExecutionProvider, ExecutionProviderOptions, RegisterError};
5use crate::{error::Result, session::builder::SessionBuilder};
6
7// https://github.com/microsoft/onnxruntime/blob/ffceed9d44f2f3efb9dd69fa75fea51163c91d91/onnxruntime/contrib_ops/cpu/bert/attention_common.h#L160-L171
8#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[repr(transparent)]
10pub struct AttentionBackend(u32);
11
12impl AttentionBackend {
13	pub const FLASH_ATTENTION: Self = Self(1 << 0);
14	pub const EFFICIENT_ATTENTION: Self = Self(1 << 1);
15	pub const TRT_FUSED_ATTENTION: Self = Self(1 << 2);
16	pub const CUDNN_FLASH_ATTENTION: Self = Self(1 << 3);
17	pub const MATH: Self = Self(1 << 4);
18
19	pub const TRT_FLASH_ATTENTION: Self = Self(1 << 5);
20	pub const TRT_CROSS_ATTENTION: Self = Self(1 << 6);
21	pub const TRT_CAUSAL_ATTENTION: Self = Self(1 << 7);
22
23	pub const LEAN_ATTENTION: Self = Self(1 << 8);
24
25	pub fn none() -> Self {
26		AttentionBackend(0)
27	}
28
29	pub fn all() -> Self {
30		Self::FLASH_ATTENTION
31			| Self::EFFICIENT_ATTENTION
32			| Self::TRT_FUSED_ATTENTION
33			| Self::CUDNN_FLASH_ATTENTION
34			| Self::MATH
35			| Self::TRT_FLASH_ATTENTION
36			| Self::TRT_CROSS_ATTENTION
37			| Self::TRT_CAUSAL_ATTENTION
38	}
39}
40
41impl BitOr for AttentionBackend {
42	type Output = Self;
43	fn bitor(self, rhs: Self) -> Self::Output {
44		Self(rhs.0 | self.0)
45	}
46}
47
48/// The type of search done for cuDNN convolution algorithms.
49#[derive(Debug, Clone, Default)]
50pub enum ConvAlgorithmSearch {
51	/// Expensive exhaustive benchmarking using [`cudnnFindConvolutionForwardAlgorithmEx`][exhaustive].
52	/// This function will attempt all possible algorithms for `cudnnConvolutionForward` to find the fastest algorithm.
53	/// Exhaustive search trades off between memory usage and speed. The first execution of a graph will be slow while
54	/// possible convolution algorithms are tested.
55	///
56	/// [exhaustive]: https://docs.nvidia.com/deeplearning/cudnn/api/index.html#cudnnFindConvolutionForwardAlgorithmEx
57	#[default]
58	Exhaustive,
59	/// Lightweight heuristic-based search using [`cudnnGetConvolutionForwardAlgorithm_v7`][heuristic].
60	/// Heuristic search sorts available convolution algorithms by expected (based on internal heuristic) relative
61	/// performance.
62	///
63	/// [heuristic]: https://docs.nvidia.com/deeplearning/cudnn/api/index.html#cudnnGetConvolutionForwardAlgorithm_v7
64	Heuristic,
65	/// Uses the default convolution algorithm, [`CUDNN_CONVOLUTION_FWD_ALGO_IMPLICIT_PRECOMP_GEMM`][fwdalgo].
66	/// The default algorithm may not have the best performance depending on specific hardware used. It's recommended to
67	/// use [`Exhaustive`] or [`Heuristic`] to search for a faster algorithm instead. However, `Default` does have its
68	/// uses, such as when available memory is tight.
69	///
70	/// > **NOTE**: This name may be confusing as this is not the default search algorithm for the CUDA EP. The default
71	/// > search algorithm is actually [`Exhaustive`].
72	///
73	/// [fwdalgo]: https://docs.nvidia.com/deeplearning/cudnn/api/index.html#cudnnConvolutionFwdAlgo_t
74	/// [`Exhaustive`]: ConvAlgorithmSearch::Exhaustive
75	/// [`Heuristic`]: ConvAlgorithmSearch::Heuristic
76	Default
77}
78
79/// [CUDA execution provider](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html) for NVIDIA
80/// CUDA-enabled GPUs.
81#[derive(Debug, Default, Clone)]
82pub struct CUDA {
83	options: ExecutionProviderOptions
84}
85
86super::impl_ep!(arbitrary; CUDA);
87
88impl CUDA {
89	/// Configures which device the EP should use.
90	///
91	/// ```
92	/// # use ort::{ep, session::Session};
93	/// # fn main() -> ort::Result<()> {
94	/// let ep = ep::CUDA::default().with_device_id(0).build();
95	/// # Ok(())
96	/// # }
97	/// ```
98	#[must_use]
99	pub fn with_device_id(mut self, device_id: i32) -> Self {
100		self.options.set("device_id", device_id.to_string());
101		self
102	}
103
104	/// Configure the size limit of the device memory arena in bytes.
105	///
106	/// This only controls how much memory can be allocated to the *arena* - actual memory usage may be higher due to
107	/// internal CUDA allocations, like those required for different [`ConvAlgorithmSearch`] options.
108	///
109	/// ```
110	/// # use ort::{ep, session::Session};
111	/// # fn main() -> ort::Result<()> {
112	/// let ep = ep::CUDA::default().with_memory_limit(2 * 1024 * 1024 * 1024).build();
113	/// # Ok(())
114	/// # }
115	/// ```
116	#[must_use]
117	pub fn with_memory_limit(mut self, limit: usize) -> Self {
118		self.options.set("gpu_mem_limit", limit.to_string());
119		self
120	}
121
122	/// Configure the strategy for extending the device's memory arena.
123	///
124	/// ```
125	/// # use ort::{ep::{self, ArenaExtendStrategy}, session::Session};
126	/// # fn main() -> ort::Result<()> {
127	/// let ep = ep::CUDA::default()
128	/// 	.with_arena_extend_strategy(ArenaExtendStrategy::SameAsRequested)
129	/// 	.build();
130	/// # Ok(())
131	/// # }
132	/// ```
133	#[must_use]
134	pub fn with_arena_extend_strategy(mut self, strategy: ArenaExtendStrategy) -> Self {
135		self.options.set(
136			"arena_extend_strategy",
137			match strategy {
138				ArenaExtendStrategy::NextPowerOfTwo => "kNextPowerOfTwo",
139				ArenaExtendStrategy::SameAsRequested => "kSameAsRequested"
140			}
141		);
142		self
143	}
144
145	/// Controls the search mode used to select a kernel for `Conv` nodes.
146	///
147	/// cuDNN, the library used by ONNX Runtime's CUDA EP for many operations, provides many different implementations
148	/// of the `Conv` node. Each of these implementations has different performance characteristics depending on the
149	/// exact hardware and model/input size used. This option controls how cuDNN should determine which implementation
150	/// to use.
151	///
152	/// The default search algorithm, [`Exhaustive`][exh], will benchmark all available implementations and use the most
153	/// performant one. This option is very resource intensive (both computationally on first run and peak-memory-wise),
154	/// but ensures best performance. It is roughly equivalent to setting `torch.backends.cudnn.benchmark = True` with
155	/// PyTorch. See also [`CUDA::with_conv_max_workspace`] to configure how much memory the exhaustive
156	/// search can use (the default is unlimited).
157	///
158	/// A less resource-intensive option is [`Heuristic`][heu]. Rather than benchmarking every implementation,
159	/// an optimal implementation is chosen based on a set of heuristics, thus saving compute. [`Heuristic`][heu] should
160	/// generally choose an optimal convolution algorithm, except in some corner cases.
161	///
162	/// [`Default`][def] can also be passed to instruct cuDNN to always use the default implementation (which is rarely
163	/// the most optimal). Note that the "Default" here refers to the **default convolution algorithm** being used, it
164	/// is not the *default behavior* (that would be [`Exhaustive`][exh]).
165	///
166	/// ```
167	/// # use ort::{ep, session::Session};
168	/// # fn main() -> ort::Result<()> {
169	/// let ep = ep::CUDA::default()
170	/// 	.with_conv_algorithm_search(ep::cuda::ConvAlgorithmSearch::Heuristic)
171	/// 	.build();
172	/// # Ok(())
173	/// # }
174	/// ```
175	///
176	/// [exh]: ConvAlgorithmSearch::Exhaustive
177	/// [heu]: ConvAlgorithmSearch::Heuristic
178	/// [def]: ConvAlgorithmSearch::Default
179	#[must_use]
180	pub fn with_conv_algorithm_search(mut self, search: ConvAlgorithmSearch) -> Self {
181		self.options.set(
182			"cudnn_conv_algo_search",
183			match search {
184				ConvAlgorithmSearch::Exhaustive => "EXHAUSTIVE",
185				ConvAlgorithmSearch::Heuristic => "HEURISTIC",
186				ConvAlgorithmSearch::Default => "DEFAULT"
187			}
188		);
189		self
190	}
191
192	/// Configure whether the [`Exhaustive`][ConvAlgorithmSearch::Exhaustive] search can use as much memory as it
193	/// needs.
194	///
195	/// The default is `true`. When `false`, the memory used for the search is limited to 32 MB, which will impact its
196	/// ability to find an optimal convolution algorithm.
197	///
198	/// ```
199	/// # use ort::{ep, session::Session};
200	/// # fn main() -> ort::Result<()> {
201	/// let ep = ep::CUDA::default().with_conv_max_workspace(false).build();
202	/// # Ok(())
203	/// # }
204	/// ```
205	#[must_use]
206	pub fn with_conv_max_workspace(mut self, enable: bool) -> Self {
207		self.options.set("cudnn_conv_use_max_workspace", if enable { "1" } else { "0" });
208		self
209	}
210
211	// Here once lied `do_copy_in_default_stream`. After reading through upstream it doesn't seem like this option is
212	// used anymore, so the setter here was removed to reduce confusion.
213
214	/// Configure whether or not to pad 3-dimensional convolutions to `[N, C, 1, D]` (as opposed to the default `[N, C,
215	/// D, 1]`).
216	///
217	/// Enabling this option might significantly improve performance on devices like the A100. This does not affect
218	/// convolution operations that do not use 3-dimensional input shapes, or the *result* of such operations.
219	///
220	/// ```
221	/// # use ort::{ep, session::Session};
222	/// # fn main() -> ort::Result<()> {
223	/// let ep = ep::CUDA::default().with_conv1d_pad_to_nc1d(true).build();
224	/// # Ok(())
225	/// # }
226	/// ```
227	#[must_use]
228	pub fn with_conv1d_pad_to_nc1d(mut self, enable: bool) -> Self {
229		self.options.set("cudnn_conv1d_pad_to_nc1d", if enable { "1" } else { "0" });
230		self
231	}
232
233	/// Configures whether to create a CUDA graph.
234	///
235	/// CUDA graphs eliminate the overhead of launching kernels sequentially by capturing the launch sequence into a
236	/// graph that is 'replayed' across runs, reducing CPU overhead and possibly improving performance.
237	///
238	/// Using CUDA graphs comes with limitations, notably:
239	/// - Models with control flow operators (like `If`, `Loop`, or `Scan`) are not supported.
240	/// - Input/output shapes cannot change across inference calls.
241	/// - The address of inputs/outputs cannot change across inference calls, so
242	///   [`IoBinding`](crate::session::IoBinding) must be used.
243	/// - `Session`s using CUDA graphs are technically not `Send` or `Sync`.
244	///
245	/// Consult the [ONNX Runtime documentation on CUDA graphs](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#using-cuda-graphs-preview) for more information.
246	///
247	/// ```
248	/// # use ort::{ep, session::Session};
249	/// # fn main() -> ort::Result<()> {
250	/// let ep = ep::CUDA::default().with_cuda_graph(true).build();
251	/// # Ok(())
252	/// # }
253	/// ```
254	#[must_use]
255	pub fn with_cuda_graph(mut self, enable: bool) -> Self {
256		self.options.set("enable_cuda_graph", if enable { "1" } else { "0" });
257		self
258	}
259
260	/// Enable 'strict' mode for `SkipLayerNorm` nodes (created via fusion of `Add` & `LayerNorm` nodes).
261	///
262	/// `SkipLayerNorm`'s strict mode trades performance for accuracy. The default is `false` (strict mode disabled).
263	///
264	/// ```
265	/// # use ort::{ep, session::Session};
266	/// # fn main() -> ort::Result<()> {
267	/// let ep = ep::CUDA::default().with_skip_layer_norm_strict_mode(true).build();
268	/// # Ok(())
269	/// # }
270	/// ```
271	#[must_use]
272	pub fn with_skip_layer_norm_strict_mode(mut self, enable: bool) -> Self {
273		self.options.set("enable_skip_layer_norm_strict_mode", if enable { "1" } else { "0" });
274		self
275	}
276
277	/// Enable the usage of the reduced-precision [TensorFloat-32](https://blogs.nvidia.com/blog/tensorfloat-32-precision-format/)
278	/// format for matrix multiplications & convolutions.
279	///
280	/// TensorFloat-32 is a reduced-precision floating point format available on NVIDIA GPUs since the Ampere
281	/// microarchitecture. It allows `MatMul` & `Conv` to run much faster on Ampere's Tensor cores. This option is
282	/// **disabled** by default.
283	///
284	/// This option is roughly equivalent to `torch.backends.cudnn.allow_tf32 = True` &
285	/// `torch.backends.cuda.matmul.allow_tf32 = True` or `torch.set_float32_matmul_precision("medium")` in PyTorch.
286	///
287	/// ```
288	/// # use ort::{ep, session::Session};
289	/// # fn main() -> ort::Result<()> {
290	/// let ep = ep::CUDA::default().with_tf32(true).build();
291	/// # Ok(())
292	/// # }
293	/// ```
294	#[must_use]
295	pub fn with_tf32(mut self, enable: bool) -> Self {
296		self.options.set("use_tf32", if enable { "1" } else { "0" });
297		self
298	}
299
300	/// Configure whether to prefer `[N, H, W, C]` layout operations over the default `[N, C, H, W]` layout.
301	///
302	/// Tensor cores usually operate more efficiently with the NHWC layout, so enabling this option for
303	/// convolution-heavy models on Tensor core-enabled GPUs may provide a significant performance improvement.
304	///
305	/// ```
306	/// # use ort::{ep, session::Session};
307	/// # fn main() -> ort::Result<()> {
308	/// let ep = ep::CUDA::default().with_prefer_nhwc(true).build();
309	/// # Ok(())
310	/// # }
311	/// ```
312	#[must_use]
313	pub fn with_prefer_nhwc(mut self, enable: bool) -> Self {
314		self.options.set("prefer_nhwc", if enable { "1" } else { "0" });
315		self
316	}
317
318	/// Use a custom CUDA device stream rather than the default one.
319	///
320	/// # Safety
321	/// The provided `stream` must outlive the environment/session configured to use this execution provider.
322	#[must_use]
323	pub unsafe fn with_compute_stream(mut self, stream: *mut ()) -> Self {
324		self.options.set("has_user_compute_stream", "1");
325		self.options.set("user_compute_stream", (stream as usize).to_string());
326		self
327	}
328
329	/// Configures the available backends used for `Attention` nodes.
330	///
331	/// ```
332	/// # use ort::{ep, session::Session};
333	/// # fn main() -> ort::Result<()> {
334	/// let ep = ep::CUDA::default()
335	/// 	.with_attention_backend(
336	/// 		ep::cuda::AttentionBackend::FLASH_ATTENTION | ep::cuda::AttentionBackend::TRT_FUSED_ATTENTION
337	/// 	)
338	/// 	.build();
339	/// # Ok(())
340	/// # }
341	/// ```
342	#[must_use]
343	pub fn with_attention_backend(mut self, flags: AttentionBackend) -> Self {
344		self.options.set("sdpa_kernel", flags.0.to_string());
345		self
346	}
347
348	#[must_use]
349	pub fn with_fuse_conv_bias(mut self, enable: bool) -> Self {
350		self.options.set("fuse_conv_bias", if enable { "1" } else { "0" });
351		self
352	}
353
354	// https://github.com/microsoft/onnxruntime/blob/ffceed9d44f2f3efb9dd69fa75fea51163c91d91/onnxruntime/core/providers/cuda/cuda_execution_provider_info.h#L48
355	// https://github.com/microsoft/onnxruntime/blob/fe8a10caa40f64a8fbd144e7049cf5b14c65542d/onnxruntime/core/providers/cuda/cuda_execution_provider_info.cc#L17
356}
357
358impl ExecutionProvider for CUDA {
359	fn name(&self) -> &'static str {
360		"CUDAExecutionProvider"
361	}
362
363	fn supported_by_platform(&self) -> bool {
364		cfg!(any(all(target_os = "linux", any(target_arch = "aarch64", target_arch = "x86_64")), all(target_os = "windows", target_arch = "x86_64")))
365	}
366
367	#[allow(unused, unreachable_code)]
368	fn register(&self, session_builder: &mut SessionBuilder) -> Result<(), RegisterError> {
369		#[cfg(any(feature = "load-dynamic", feature = "cuda"))]
370		{
371			use core::ptr;
372
373			use crate::{AsPointer, ortsys, util};
374
375			let mut cuda_options: *mut ort_sys::OrtCUDAProviderOptionsV2 = ptr::null_mut();
376			ortsys![unsafe CreateCUDAProviderOptions(&mut cuda_options)?];
377			let _guard = util::run_on_drop(|| {
378				ortsys![unsafe ReleaseCUDAProviderOptions(cuda_options)];
379			});
380
381			let ffi_options = self.options.to_ffi();
382			ortsys![unsafe UpdateCUDAProviderOptions(
383				cuda_options,
384				ffi_options.key_ptrs(),
385				ffi_options.value_ptrs(),
386				ffi_options.len()
387			)?];
388
389			ortsys![unsafe SessionOptionsAppendExecutionProvider_CUDA_V2(session_builder.ptr_mut(), cuda_options)?];
390			return Ok(());
391		}
392
393		Err(RegisterError::MissingFeature)
394	}
395}
396
397// Take care in how these are ordered, since some of them depend on each other. Dependencies need to be loaded before
398// their dependents.
399#[cfg(windows)]
400pub const CUDA_DYLIBS: &[&str] = &["cudart64_12.dll", "cublasLt64_12.dll", "cublas64_12.dll", "cufft64_11.dll"];
401#[cfg(not(windows))]
402pub const CUDA_DYLIBS: &[&str] = &["libcudart.so.12", "libcublasLt.so.12", "libcublas.so.12", "libnvrtc.so.12", "libcurand.so.10", "libcufft.so.11"];
403
404#[cfg(windows)]
405pub const CUDNN_DYLIBS: &[&str] = &[
406	"cudnn64_9.dll",
407	"cudnn_graph64_9.dll",
408	"cudnn_ops64_9.dll",
409	"cudnn_heuristic64_9.dll",
410	"cudnn_adv64_9.dll",
411	"cudnn_cnn64_9.dll",
412	"cudnn_engines_precompiled64_9.dll",
413	"cudnn_engines_runtime_compiled64_9.dll"
414];
415#[cfg(not(windows))]
416pub const CUDNN_DYLIBS: &[&str] = &[
417	"libcudnn.so.9",
418	"libcudnn_graph.so.9",
419	"libcudnn_ops.so.9",
420	"libcudnn_heuristic.so.9",
421	"libcudnn_adv.so.9",
422	"libcudnn_cnn.so.9",
423	"libcudnn_engines_precompiled.so.9",
424	"libcudnn_engines_runtime_compiled.so.9"
425];
426
427/// Preload the dylibs required by CUDA/cuDNN.
428///
429/// This attempts to load all dynamic libraries required by the CUDA execution provider from the given CUDA and cuDNN
430/// directories, if they are provided. Passing `None` will prevent preloading binaries for that component. This function
431/// will immediately return with an error when a library fails to load, without attempting to load the rest of the
432/// libraries.
433///
434/// Preloading a library in this way will prioritize it in the search order when the CUDA EP attempts to load its
435/// dependencies, effectively allowing you to customize the CUDA install path without modifying the `PATH` environment
436/// variable. Note that this function intentionally leaks memory; see [`crate::util::preload_dylib`] for more
437/// information.
438///
439/// ```
440/// # use std::path::Path;
441/// use ort::ep;
442///
443/// let cuda_root = Path::new(r#"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6\bin"#);
444/// let cudnn_root = Path::new(r#"D:\cudnn_9.8.0"#);
445///
446/// // Load CUDA & cuDNN
447/// let _ = ep::cuda::preload_dylibs(Some(cuda_root), Some(cudnn_root));
448///
449/// // Only preload cuDNN
450/// let _ = ep::cuda::preload_dylibs(None, Some(cudnn_root));
451/// ```
452#[cfg_attr(docsrs, doc(cfg(any(feature = "preload-dylibs", feature = "load-dynamic"))))]
453#[cfg(all(feature = "preload-dylibs", not(target_arch = "wasm32")))]
454pub fn preload_dylibs(cuda_root_dir: Option<&std::path::Path>, cudnn_root_dir: Option<&std::path::Path>) -> Result<()> {
455	use crate::util::preload_dylib;
456	if let Some(cuda_root_dir) = cuda_root_dir {
457		for dylib in CUDA_DYLIBS {
458			preload_dylib(cuda_root_dir.join(dylib)).map_err(|e| crate::Error::new(format!("Failed to preload `{dylib}`: {e}")))?;
459		}
460	}
461	if let Some(cudnn_root_dir) = cudnn_root_dir {
462		for dylib in CUDNN_DYLIBS {
463			preload_dylib(cudnn_root_dir.join(dylib)).map_err(|e| crate::Error::new(format!("Failed to preload `{dylib}`: {e}")))?;
464		}
465	}
466	Ok(())
467}