Skip to main content

ort/ep/
cuda.rs

1use alloc::string::ToString;
2use core::{ops::BitOr, ptr};
3
4use super::{ArenaExtendStrategy, ExecutionProvider, ExecutionProviderOptions};
5use crate::{AsPointer, error::Result, ortsys, session::builder::SessionBuilder, util};
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 register(&self, session_builder: &mut SessionBuilder) -> Result<()> {
364		let mut cuda_options: *mut ort_sys::OrtCUDAProviderOptionsV2 = ptr::null_mut();
365		ortsys![unsafe CreateCUDAProviderOptions(&mut cuda_options)?];
366		let _guard = util::run_on_drop(|| {
367			ortsys![unsafe ReleaseCUDAProviderOptions(cuda_options)];
368		});
369
370		let ffi_options = self.options.to_ffi();
371		ortsys![unsafe UpdateCUDAProviderOptions(
372				cuda_options,
373				ffi_options.key_ptrs(),
374				ffi_options.value_ptrs(),
375				ffi_options.len()
376			)?];
377
378		ortsys![unsafe SessionOptionsAppendExecutionProvider_CUDA_V2(session_builder.ptr_mut(), cuda_options)?];
379
380		Ok(())
381	}
382}
383
384// Take care in how these are ordered, since some of them depend on each other. Dependencies need to be loaded before
385// their dependents.
386#[cfg(windows)]
387pub const CUDA_DYLIBS: &[&str] = &["cudart64_12.dll", "cublasLt64_12.dll", "cublas64_12.dll", "cufft64_11.dll"];
388#[cfg(not(windows))]
389pub const CUDA_DYLIBS: &[&str] = &["libcudart.so.12", "libcublasLt.so.12", "libcublas.so.12", "libnvrtc.so.12", "libcurand.so.10", "libcufft.so.11"];
390
391#[cfg(windows)]
392pub const CUDNN_DYLIBS: &[&str] = &[
393	"cudnn64_9.dll",
394	"cudnn_graph64_9.dll",
395	"cudnn_ops64_9.dll",
396	"cudnn_heuristic64_9.dll",
397	"cudnn_adv64_9.dll",
398	"cudnn_cnn64_9.dll",
399	"cudnn_engines_precompiled64_9.dll",
400	"cudnn_engines_runtime_compiled64_9.dll"
401];
402#[cfg(not(windows))]
403pub const CUDNN_DYLIBS: &[&str] = &[
404	"libcudnn.so.9",
405	"libcudnn_graph.so.9",
406	"libcudnn_ops.so.9",
407	"libcudnn_heuristic.so.9",
408	"libcudnn_adv.so.9",
409	"libcudnn_cnn.so.9",
410	"libcudnn_engines_precompiled.so.9",
411	"libcudnn_engines_runtime_compiled.so.9"
412];
413
414/// Preload the dylibs required by CUDA/cuDNN.
415///
416/// This attempts to load all dynamic libraries required by the CUDA execution provider from the given CUDA and cuDNN
417/// directories, if they are provided. Passing `None` will prevent preloading binaries for that component. This function
418/// will immediately return with an error when a library fails to load, without attempting to load the rest of the
419/// libraries.
420///
421/// Preloading a library in this way will prioritize it in the search order when the CUDA EP attempts to load its
422/// dependencies, effectively allowing you to customize the CUDA install path without modifying the `PATH` environment
423/// variable. Note that this function intentionally leaks memory; see [`crate::util::preload_dylib`] for more
424/// information.
425///
426/// ```
427/// # use std::path::Path;
428/// use ort::ep;
429///
430/// let cuda_root = Path::new(r#"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6\bin"#);
431/// let cudnn_root = Path::new(r#"D:\cudnn_9.8.0"#);
432///
433/// // Load CUDA & cuDNN
434/// let _ = ep::cuda::preload_dylibs(Some(cuda_root), Some(cudnn_root));
435///
436/// // Only preload cuDNN
437/// let _ = ep::cuda::preload_dylibs(None, Some(cudnn_root));
438/// ```
439#[cfg_attr(docsrs, doc(cfg(any(feature = "preload-dylibs", feature = "load-dynamic"))))]
440#[cfg(all(feature = "preload-dylibs", not(target_arch = "wasm32")))]
441pub fn preload_dylibs(cuda_root_dir: Option<&std::path::Path>, cudnn_root_dir: Option<&std::path::Path>) -> Result<()> {
442	use crate::util::preload_dylib;
443	if let Some(cuda_root_dir) = cuda_root_dir {
444		for dylib in CUDA_DYLIBS {
445			preload_dylib(cuda_root_dir.join(dylib)).map_err(|e| crate::Error::new(format!("Failed to preload `{dylib}`: {e}")))?;
446		}
447	}
448	if let Some(cudnn_root_dir) = cudnn_root_dir {
449		for dylib in CUDNN_DYLIBS {
450			preload_dylib(cudnn_root_dir.join(dylib)).map_err(|e| crate::Error::new(format!("Failed to preload `{dylib}`: {e}")))?;
451		}
452	}
453	Ok(())
454}
455
456pub mod resource {
457	use core::{ffi::c_void, ptr::NonNull};
458
459	use crate::ep::ExecutionProviderResource;
460
461	macro_rules! define_resource {
462		($name:ident<$ty:ty>($id:literal), $conv:expr) => {
463			pub struct $name;
464			impl ExecutionProviderResource for $name {
465				const VERSION: u32 = 3;
466				type Type = $ty;
467				fn id(&self) -> u32 {
468					$id
469				}
470				fn convert(v: *const c_void) -> Self::Type {
471					($conv)(v)
472				}
473			}
474		};
475	}
476
477	define_resource!(Stream<*const ()>(10000), |v: *const c_void| v.cast());
478	define_resource!(CuDNNHandle<*const ()>(10001), |v: *const c_void| v.cast());
479	define_resource!(CuBLASHandle<*const ()>(10002), |v: *const c_void| v.cast());
480	define_resource!(
481		DeferredCPUAllocator<Option<crate::memory::Allocator>>(10003),
482		|v: *const c_void| NonNull::new(v.cast_mut().cast()).map(|alloc| unsafe { crate::memory::Allocator::from_raw(alloc, true) })
483	);
484	define_resource!(DeviceId<u32>(10004), |v: *const c_void| v as usize as u32);
485	define_resource!(ArenaExtendStrategy<super::ArenaExtendStrategy>(10005), |v: *const c_void| match v as usize {
486		1 => super::ArenaExtendStrategy::SameAsRequested,
487		_ => super::ArenaExtendStrategy::NextPowerOfTwo
488	});
489	define_resource!(ConvAlgorithmSearch<super::ConvAlgorithmSearch>(10006), |v: *const c_void| match v as usize {
490		1 => super::ConvAlgorithmSearch::Heuristic,
491		2 => super::ConvAlgorithmSearch::Default,
492		_ => super::ConvAlgorithmSearch::Exhaustive
493	});
494	define_resource!(ConvUseMaxWorkspace<bool>(10007), |v: *const c_void| !v.is_null());
495	define_resource!(Conv1dPadToNc1d<bool>(10008), |v: *const c_void| !v.is_null());
496	define_resource!(EnableSkipLayerNormStrictMode<bool>(10009), |v: *const c_void| !v.is_null());
497	define_resource!(PreferNhwc<bool>(10010), |v: *const c_void| !v.is_null());
498	define_resource!(UseTF32<bool>(10011), |v: *const c_void| !v.is_null());
499	define_resource!(FuseConvBias<bool>(10012), |v: *const c_void| !v.is_null());
500}