docs/CUDA_cuDNN_Optional_Design.md
Status: Draft / Proposal
Owner: (CUDA EP)
Scope: onnxruntime/core/providers/cuda (main static CUDA EP), CUDA Plugin EP
(BUILD_CUDA_EP_AS_PLUGIN), and the CUDA unit tests. Out of scope: TensorRT EP and
NV‑TensorRT‑RTX EP (they create and own their own cuDNN handles and inherently depend on
cuDNN/TensorRT). The existing build-time USE_CUDA_MINIMAL path is also out of scope; it is
used by TensorRT / NV‑TensorRT‑RTX integration and should remain available.
Today the CUDA EP has a hard link‑time and load‑time dependency on cuDNN
(libcudnn*.so / cudnn64_*.dll) and on the header‑only cudnn_frontend library. If the
cuDNN shared libraries are not present on the machine, the ORT shared library
(libonnxruntime.so / onnxruntime.dll, or the CUDA provider DLL) fails to load at all —
even for models that use no cuDNN‑backed operators.
cuDNN is large (hundreds of MB across its sub‑libraries) and is only needed by a subset of operators (Conv, Pooling, BatchNorm, LRN, RNN/LSTM/GRU, the cuDNN reduction path, the cuDNN softmax path, etc.). Many transformer / LLM models do not require any of these.
Goal: Ship a single CUDA EP binary that
This is delivered in three phases:
NOT_IMPLEMENTED ("cuDNN is required for operator X but was
not found") error at Run time. Everything that does not need cuDNN runs normally.cmake/onnxruntime_providers_cuda.cmake:
target_link_libraries(... CUDNN::cudnn_all cudnn_frontend ...) — a normal link dependency,
resolved at process load time.cmake/onnxruntime_providers_cuda_plugin.cmake: same (CUDNN::cudnn_all, cudnn_frontend).cmake/onnxruntime_unittests.cmake: links cudnn_frontend and includes CUDNN_INCLUDE_DIR.cmake/deps.txt: pins cudnn_frontend v1.x (header‑only C++ wrapper over the cuDNN v9
backend API).cmake/external/cudnn_frontend.cmake fetches cudnn_frontend, sets CUDNN_PATH from
onnxruntime_CUDNN_HOME, disables its samples/tests/python bindings, and marks its headers
as system includes. In ORT this is a compile-time header dependency, not a runtime DLL.CudaStream constructor — cudnnCreate(&cudnn_handle_) / cudnnSetStream(...)
(onnxruntime/core/providers/cuda/cuda_stream_handle.cc).CUDAExecutionProvider::PerThreadContext — cudnnCreate(&cudnn_handle_)
(onnxruntime/core/providers/cuda/cuda_execution_provider.cc).cuda_stream_plugin.cc and cuda_kernel_adapter.h.CudaKernel::GetCudnnHandle(context) → stream->cudnn_handle_
(onnxruntime/core/providers/cuda/cuda_kernel.h).CUDAExecutionProvider::PerThreadDefaultCudnnHandle().CudaContext resource API (cuda_context.h,
CudaResource::cudnn_handle_t) — used by custom ops.CUDNN_CALL / CUDNN_CALL_THROW (in shared_inc/cuda_call.h) and
CUDNN_RETURN_IF_ERROR / CUDNN2_RETURN_IF_ERROR (in cuda_common.h).CUDNN_FE_CALL / CUDNN_FE_RETURN_IF_ERROR for the frontend (cudnn_fe_call.*).CudaErrString<cudnnStatus_t> calls cudnnGetErrorString (cuda_call.cc).#ifndef USE_CUDA_MINIMAL in shared CUDA code. That
build‑time path is used by TensorRT / NV‑TensorRT‑RTX related builds and is a useful
inventory of cuDNN‑touching code, but it is not the CUDA EP runtime behavior we want.| Area | Files | cuDNN usage |
|---|---|---|
| Conv / ConvTranspose (v9 graph) | nn/conv.cc, nn/conv_transpose.cc | cudnn_frontend graph API + cudnnAddTensor |
| Conv / ConvTranspose (legacy) | nn/conv_8.h, nn/conv_transpose_8.h | cudnnConvolutionForward, cudnnConvolutionBackwardData, algo search |
| FusedConv (contrib) | contrib_ops/cuda/fused_conv.cc | cudnnConvolutionBiasActivationForward, activation desc |
| Pooling | nn/pool.cc | cudnnPoolingForward, pooling desc |
| BatchNormalization | nn/batch_norm.cc | cudnnBatchNormalizationForwardInference/Training |
| InstanceNormalization | nn/instance_norm.cc | BatchNorm training helper |
| LRN | nn/lrn.cc | cudnnLRNCrossChannelForward |
| RNN / LSTM / GRU | rnn/cudnn_rnn_base.*, rnn/{rnn,lstm,gru}.h | cudnnRNNForward, RNN/dropout descriptors |
| Reductions | reduction/reduction_ops.* | cudnnReduceTensor (Reduce*, ArgMax/ArgMin) |
| Softmax (cuDNN path) | math/softmax_common.cc | cudnnSoftmaxForward/Backward |
| Einsum | math/einsum_utils/* | passes cuDNN handle into helpers |
| Dropout descriptor | cudnn_common.h (CudnnDropout) | used by RNN |
| Tensor/Filter descriptors | cudnn_common.* | cudnnCreate*Descriptor, cudnnSet*Descriptor |
| Contrib attention (optional) | contrib_ops/cuda/bert/group_query_attention.cc, quantization/attention_quantization.cc, math/bias_softmax.cc | optional cuDNN flash attention / handle passthrough |
Note: Several of these ops already have, or can trivially get, a non‑cuDNN path (e.g. Softmax has a native warp/block kernel; reductions have CUB‑based paths; pooling and simple elementwise norms are straightforward). Softmax and reductions are Phase‑2 candidates; the remaining NN ops are Phase‑3 candidates.
cudnn_frontend usagecudnn_frontend is currently used as a header-only C++ graph API wrapper around cuDNN's
backend API:
onnxruntime/core/providers/cuda/nn/conv.h includes <cudnn_frontend.h> and stores
cudnn_frontend::graph::Graph, Tensor_attributes, Pointwise_attributes, and variant
packs in CudnnConvState.onnxruntime/core/providers/cuda/nn/conv.cc builds cuDNN frontend graphs for v9 Conv,
optional bias fusion, optional activation fusion, heuristic selection, support checks, plan
building, workspace sizing, and graph execution.onnxruntime/core/providers/cuda/nn/conv_transpose.cc does the same for ConvTranspose
using Conv_dgrad_attributes.onnxruntime/core/providers/cuda/cudnn_common.{h,cc} defines CudnnFeTensor, a small ORT
helper that maps ORT tensor shapes/types into cudnn_frontend::graph::Tensor_attributes.onnxruntime/core/providers/cuda/shared_inc/cudnn_fe_call.h and
onnxruntime/core/providers/cuda/cudnn_fe_call.cc adapt cudnn_frontend::error_t into
ORT's CudaCall error-handling path.onnxruntime/contrib_ops/cuda/bert/group_query_attention.cc has a cuDNN SDPA feature path
selected through kernel options, but it does not directly include the high-level frontend
graph headers in the same way Conv/ConvTranspose do.Important frontend detail: cudnn_frontend already has a dynamic-loading mode gated by
NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING. In that mode, cudnn_frontend_shim.h does not
link directly against cudnnBackend* symbols. Instead, it expects the embedding library to
define cudnn_frontend::cudnn_dlhandle, then resolves symbols from that handle with
dlsym / GetProcAddress.
The frontend backend-symbol surface used by ORT's current graph path includes at least:
cudnnGetVersion, cudnnGetErrorString, and for cuDNN 9, cudnnGetLastErrorString.cudnnBackendCreateDescriptor, cudnnBackendDestroyDescriptor,
cudnnBackendSetAttribute, cudnnBackendGetAttribute, and cudnnBackendFinalize.cudnnBackendExecute.cudnnBackendPopulateCudaGraph,
cudnnBackendUpdateCudaGraph, and cudnnGetExecutionPlanWorkspaceSize if ORT starts using
frontend features that require them.Therefore, cudnn_frontend should stay in the build while ORT still has cuDNN frontend
Conv/ConvTranspose paths, but it should be compiled in dynamic-loading mode and wired to the
same ORT-owned cuDNN loader used by direct cuDNN calls.
The core idea: break the hard dependency by routing every cuDNN symbol through a thin, lazily‑resolved trampoline layer, plus an availability flag the EP and kernels consult.
flowchart TD
K[CUDA kernel
e.g. Conv, Pool] -->|cudnnXxx(...)| S[cuDNN shim
trampolines]
FE[cudnn_frontend
header-only] -->|dynamic mode dlsym(cudnn_dlhandle)| L
S -->|first call: dlopen/LoadLibrary| L[cuDNN loader]
L -->|present| R[(real libcudnn*)]
L -->|absent| U[mark unavailable
return sentinel status]
K -.->|enable_cudnn && IsCudnnAvailable()?| L
We stop linking CUDNN::cudnn_all. In its place we compile a generated translation unit,
cudnn_stub.cc, that defines every direct cuDNN entry point ORT references. Each
definition is a trampoline:
// Pseudocode for one entry
cudnnStatus_t cudnnConvolutionForward(cudnnHandle_t h, /* ... */) {
auto fn = CudnnLibrary::Get().convolution_forward; // resolved lazily
if (fn == nullptr) return CUDNN_STATUS_NOT_INITIALIZED; // cuDNN unavailable
return fn(h, /* ... */);
}
Because the trampolines have the exact cuDNN symbol names and signatures, ORT's direct
calls link against our definitions — so the final binary has no NEEDED/import entry for
libcudnn from those calls.
cudnn_frontend is handled separately: compile it with
NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING and define cudnn_frontend::cudnn_dlhandle in ORT.
When the ORT loader successfully opens cuDNN, it sets that handle to the loaded cuDNN library
handle. cudnn_frontend then resolves cudnnBackend* symbols from the same handle. When
enable_cudnn=0 or cuDNN is unavailable, ORT must guard all frontend graph-build/execute
entry points before calling frontend APIs so the frontend shim never tries to resolve symbols
from a null handle.
This means cudnn_frontend stays a compile‑time‑only dependency. We still need its headers
and the cuDNN headers to build, but not cuDNN import libraries at link time.
A single loader object owns the dlopen/LoadLibrary handles and the resolved function
pointers:
class CudnnLibrary { // onnxruntime/core/providers/cuda/cudnn_loader.{h,cc}
public:
static CudnnLibrary& Get(); // thread-safe singleton (std::call_once)
bool Available() const; // true iff all required libs + symbols resolved
// ... function pointer members, one per cuDNN entry ORT/frontend uses ...
};
Loader responsibilities:
libcudnn.so.9 umbrella plus, depending on packaging,
libcudnn_graph, libcudnn_ops, libcudnn_cnn, libcudnn_engines_*). On Windows the
corresponding cudnn*64_9.dll set. We load the umbrella libcudnn first; cuDNN itself
dlopens its sub‑libraries.dlsym / GetProcAddress.available_ = false.On Windows, cuDNN 9 is split into multiple DLLs. The loader should not rely on the process
working directory. Application or package code that needs an explicit directory should use a
trusted process-level preload mechanism, such as Python preload_dlls(cudnn=True, directory=...), before creating the session. On Linux, the C++ loader uses the default
dynamic loader search behavior for the umbrella libcudnn.so.9; deployment should provide
trusted library paths via the system loader, container image, package manager, or explicit
application preload.
The loader must not run when the CUDA provider option enable_cudnn=0 is set (see §3.3).
This keeps "force no cuDNN" tests deterministic even on machines where cuDNN is installed.
Symbol manifest. The set of symbols is finite and enumerable (see §2.4 plus
cudnn_common.* and cudnn_rnn_base.*). We maintain direct ORT cuDNN calls as a single
header list (cudnn_symbols.inc, an X‑macro list) consumed by both the loader and the stub
generator so the two never drift. Frontend backend symbols are resolved by
cudnn_frontend's own dynamic-loading shim from the same ORT-owned cuDNN handle; maintain a
separate frontend-symbol audit list for testing and version checks.
Alternative considered (delay‑load only): Windows
/DELAYLOAD:cudnn*.dllgets us lazy load on Windows, but Linux has no equivalent, andcudnn_frontendrequires explicit dynamic loading support to avoid backend API imports. Rejected because the request requires identical behavior on Linux and Windows from a single binary. The direct-call trampoline plus frontend dynamic-loading approach is uniform across both.
cudnnCreate is only invoked through the shim. The eager cudnnCreate calls in
CudaStream / PerThreadContext / plugin stream become conditional and non‑fatal:
CudnnLibrary::Get().Available(); if false, leave cudnn_handle_ == nullptr and
do not throw.GetCudnnHandle(context) returns nullptr when cuDNN is unavailable (it already returns a
raw handle; today it's never null).cuda_common.h:bool CudnnAvailable(const OpKernelContext* context); // provider option && runtime availability
// For kernels: fail fast with a clear message if cuDNN is required but missing.
#define ORT_RETURN_IF_CUDNN_UNAVAILABLE(context, op_name) \
ORT_RETURN_IF_ERROR(::onnxruntime::cuda::CheckCudnnAvailable(context, op_name))
CudaErrString<cudnnStatus_t> must not call cudnnGetErrorString when cuDNN is unavailable
(route through the shim, which returns a static string in that case).enable_cudnncuDNN can be disabled explicitly with a CUDA provider option:
enable_cudnn = 1 # default: try to load and use cuDNN when it is present
enable_cudnn = 0 # do not load cuDNN; force native CUDA paths / Phase-1 NOT_IMPLEMENTED
enable_cudnn is the policy switch. When it is 0, ORT must not attempt to load cuDNN. ORT
intentionally does not provide a cudnn_path provider option because provider options can be
supplied by higher-level configuration systems, and allowing them to choose a native DLL/SO
path would create a library-loading security risk.
Implementation details:
constexpr const char* kEnableCudnn = "enable_cudnn" in
cuda::provider_option_names.bool enable_cudnn{true}; to CUDAExecutionProviderInfo.ProviderOptionsParser::AddAssignmentToReference(...) in
CUDAExecutionProviderInfo::FromProviderOptions(...).CUDAExecutionProviderInfo::ToProviderOptions(...).std::hash<CUDAExecutionProviderInfo> because it changes the EP behavior.OrtCUDAProviderOptionsV2 for Phase 1. That struct is public C
ABI surface; string-key provider options are sufficient and can be set through existing
provider-options APIs.CUDAExecutionProvider::IsCudnnEnabled() or
CudaKernel::IsCudnnEnabled() so kernels can distinguish:
enable_cudnn=0), andThe effective condition for cuDNN use is:
effective_cudnn_available = info.enable_cudnn && CudnnLibrary::Get().Available()
If enable_cudnn=0, ORT must not call dlopen / LoadLibrary for cuDNN and must not create
a cuDNN handle. If enable_cudnn=1, ORT uses trusted process-level library discovery: the
system loader search path, package/container deployment, or an explicit preload performed by
application code before session creation.
Per the agreed design, in Phase 1 cuDNN‑dependent kernels remain registered but fail
fast when executed without cuDNN. The check is added at the top of each cuDNN op's
ComputeInternal (or centralized in shared base helpers such as CudnnConvState setup,
CudnnRnnBase, the reduction helper, etc.):
Status Conv<T>::ComputeInternal(OpKernelContext* context) const {
ORT_RETURN_IF_CUDNN_UNAVAILABLE(context, "Conv");
// ... existing cuDNN path ...
}
The guard should include the reason in the message:
enable_cudnn=0: "Operator 'Conv' on the CUDA EP requires cuDNN, but cuDNN was disabled
by the CUDA provider option 'enable_cudnn'."Rationale for "throw" over "don't register / fall back to CPU":
BUILD_CUDA_EP_AS_PLUGIN) — same shim/loader; the plugin's
cuda_stream_plugin.cc / cuda_kernel_adapter.h handle creation becomes conditional.Python wheel packaging is unchanged by this design: cuDNN DLLs are not packed in the wheel today, so Phase 1 is not introducing a new "CUDA-minimal" wheel flavor. The runtime loader simply makes the existing package tolerant of environments where cuDNN is absent.
TensorRT / NV‑RTX EPs are untouched and continue to link cuDNN as before. (If both a TRT EP and the shimmed CUDA EP are in the same process, symbol collision must be avoided — see §8 Risks.)
The identical shim + lazy‑loader technique is used to drop the hard dependency on cuFFT
(libcufft.so.* / cufft64_*.dll). cuFFT is only needed by the FFT contrib operators
(Rfft / Irfft in contrib_ops/cuda/math), which use a small, enumerable set of entry
points: cufftCreate, cufftDestroy, cufftSetStream, cufftXtMakePlanMany, and
cufftXtExec.
Implementation:
onnxruntime/core/providers/cuda/cufft_loader.{h,cc} defines a
CufftLibrary singleton that mirrors CudnnLibrary: it lazily dlopen/LoadLibrarys the
cuFFT runtime, resolves symbols on demand, caches them, and exposes Available() / Error().
It uses the same security‑conscious search behavior as the cuDNN loader (Windows
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS plus a PATH fallback that never loads from the current
working directory). The candidate library name is selected at compile time from the
CUDA_VERSION macro (cuFFT 12 for CUDA 13.x, cuFFT 11 for CUDA 12.x), because cuFFT's
SONAME/DLL version tracks the CUDA major version. Unsupported CUDA major versions fail at
compile time until their cuFFT library mapping is added explicitly.onnxruntime/core/providers/cuda/cufft_stub.cc defines the five entry points
above as trampolines that forward to the resolved symbol (returning CUFFT_INTERNAL_ERROR
when the library is unavailable), so ORT's direct calls link against our definitions and
the final binary has no import entry for libcufft.CudaCall<cufftResult> (in cuda_call.cc) and the plugin's
CUFFT_RETURN_IF_ERROR macro check CufftLibrary::Available() and return a clear
NOT_IMPLEMENTED status ("cuFFT is unavailable …") when cuFFT is missing, exactly like the
cuDNN path.Unlike cuDNN, cuFFT needs no provider option (there is no enable_cufft): it is always
loaded lazily on first FFT‑op use, and there is no cufft_frontend equivalent to configure.
cuFFT headers (part of the CUDA toolkit) are still required at build time, but CUDA::cufft
is no longer linked in onnxruntime_providers_cuda.cmake,
onnxruntime_providers_cuda_plugin.cmake, or the CUDA unit‑test target.
Outcome: ORT CUDA EP loads and runs without cuDNN. cuDNN‑backed ops throw a clear
NOT_IMPLEMENTED error when cuDNN is absent; everything else runs normally. When cuDNN is
present, behavior is byte‑for‑byte identical to today.
Symbol inventory & manifest.
cudnn* symbol referenced by ORT code (cudnnCreate,
descriptor APIs, legacy conv APIs, BN/LRN/pooling/reduction/softmax APIs, RNN APIs,
etc.).cudnn_symbols.inc (X‑macro: name, return type, signature).cudnn_frontend backend-symbol surface resolved by its dynamic
shim: cudnnGetVersion, cudnnGetErrorString, cudnnBackend* descriptor APIs,
cudnnBackendExecute, and version-gated graph helpers such as
cudnnBackendPopulateCudaGraph, cudnnBackendUpdateCudaGraph, and
cudnnGetExecutionPlanWorkspaceSize.nm -D/dumpbin of the real cuDNN to ensure completeness; add a frontend
graph-build/execute smoke test to prove NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING resolves
backend symbols through ORT's loaded cuDNN handle.Loader (cudnn_loader.{h,cc}).
dlopen/LoadLibrary of the cuDNN umbrella lib with versioned name candidates
(libcudnn.so.9, libcudnn.so, cudnn64_9.dll, …).preload_dlls(cudnn=True, directory=...) from trusted
application code before creating a CUDA EP session.Available() + thread‑safe one‑time init; report loader diagnostics without exposing a
provider-controlled library path option.cudnn_frontend dynamic-loading mode.cudnn_frontend::cudnn_dlhandle in one ORT translation unit when
NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING is enabled. Set it to the loader's cuDNN handle
after a successful load; keep it null when cuDNN is disabled or unavailable.enable_cudnn=0, skip loader
initialization entirely and report "disabled by provider option" to the error helper.Stub/trampoline TU (cudnn_stub.cc).
cudnnStatus_t when unavailable.cudnnStatus_t entries (cudnnGetErrorString, cudnnGetVersion).The stubs must be compiled into the same target that currently links cuDNN. For Linux,
prefer hidden visibility for the stub definitions where possible to avoid exporting cuDNN
names from ORT provider binaries. The loader should use RTLD_LOCAL when opening cuDNN.
CMake changes.
CUDNN::cudnn_all from target_link_libraries for the CUDA EP, plugin EP, and
tests; keep CUDNN_INCLUDE_DIR (headers) and cudnn_frontend (headers).cudnn_stub.cc + cudnn_loader.cc into the EP.cudnn_frontend with
NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING. This is required so frontend graph code uses
dlsym / GetProcAddress on cudnn_frontend::cudnn_dlhandle instead of creating
link-time imports for cudnnBackend* symbols.USE_CUDA_MINIMAL working. It is used by TensorRT / NV‑TensorRT‑RTX related
builds and is not replaced by the optional-cuDNN runtime shim.Current CMake anchor points:
cmake/onnxruntime_providers_cuda.cmake: replace CUDNN::cudnn_all with the shim
sources/library while retaining include(cudnn_frontend) and the cuDNN include dirs;
add NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING for the provider target.cmake/onnxruntime_providers_cuda_plugin.cmake: same for
onnxruntime_providers_cuda_plugin.cmake/onnxruntime_unittests.cmake: unit tests should link against the shim path, not
against cuDNN import libraries, and should use the same frontend dynamic-loading define.cmake/onnxruntime_python.cmake: on Windows, the generated version_info.py currently
searches for cudnn64_*.dll and fails if it is missing. That fatal check must be
relaxed because cuDNN is optional; cudnn_version should be omitted, set to None, or
set to "optional" when no DLL is found.Conditional handle creation.
cuda_stream_handle.cc, cuda_execution_provider.cc, cuda_stream_plugin.cc,
cuda_kernel_adapter.h: create the handle only if
info.enable_cudnn && CudnnLibrary::Get().Available(), otherwise leave it null and do
not throw.info.enable_cudnn is false, skip the loader call entirely.CudaResource::cudnn_handle_t may be nullptr.
Any internal custom-op adapter that assumes a non-null handle must return the same clear
cuDNN-required error.Guard all cuDNN op entry points.
ORT_RETURN_IF_CUDNN_UNAVAILABLE(context, "<Op>") or an equivalent helper to the
ComputeInternal of every op in the §2.4 table (centralize in shared bases where
possible: CudnnRnnBase, conv state setup, reduction helper, pooling,
batch/instance norm, LRN, cuDNN softmax path).cudnn_frontend's
dynamic shim throws if it cannot resolve backend symbols, so ORT should fail with the
clearer provider-option / cuDNN-missing message before calling validate(),
build_operation_graph(), create_execution_plans(), check_support(),
build_plans(), or execute().Provider-option plumbing.
enable_cudnn in CUDAExecutionProviderInfo.GetProviderOptions() / ToProviderOptions().enable_cudnn default true, "0" false, "1" true, invalid
values rejected.Error‑string safety.
CudaErrString<cudnnStatus_t> shim‑safe.CudaErrString<cudnn_frontend::error_t> report frontend dynamic-loading failures
without assuming cuDNN is available.Docs & messaging.
enable_cudnn.onnxruntime.preload_dlls(cuda=True, cudnn=True, directory=...): users can still preload a known cuDNN directory, but preloading is now
optional for CUDA EP load because the provider itself lazy-loads cuDNN.onnxruntime/__init__.py behavior as needed so missing cuDNN does not produce a
scary install warning by default in optional-cuDNN packages. If the user explicitly calls
preload_dlls(cudnn=True), keep diagnostics useful and include the missing DLL name.CI workflow for no-cuDNN builds.
cudnn_frontend headers.readelf -d / ldd on Linux, dumpbin /dependents on Windows).NOT_IMPLEMENTED error rather than a dynamic-loader failure.libonnxruntime/CUDA provider loads; a model with no cuDNN ops runs correctly on CUDA.NOT_IMPLEMENTED message, not a
crash or loader error.enable_cudnn=0:
NOT_IMPLEMENTED message.Outcome: LLM‑focused CUDA workloads can run without cuDNN for common Softmax / LogSoftmax and reduction patterns. This phase is part of the first milestone with Phase 1: Phase 1 makes cuDNN optional, and Phase 2 removes the cuDNN dependency from ops that LLM models may still use.
cudnnReduceTensor dependency.For these ops, prefer the native implementation regardless of cuDNN availability, unless a specific cuDNN fast path is intentionally kept behind an opt‑in provider option:
use native CUDA / CUB implementation
optional: if provider option requests cuDNN fast path and cuDNN is available, use cuDNN
The important Phase‑2 property is that these ops must not require a non-null cuDNN handle.
If enable_cudnn=0, or if cuDNN is absent, they should still run through the native path.
NOT_IMPLEMENTED).enable_cudnn=0, no dynamic cuDNN load is attempted.Outcome: Broader CNN / vision-style CUDA workloads can run without cuDNN where practical. These ops are outside the first Phase 1 + Phase 2 milestone because they are less central to LLM workloads and, for convolution, much more expensive to replace well.
RNN / LSTM / GRU are intentionally not part of the Phase‑3 scope for now. They are the
heaviest to replace and may remain cuDNN‑only with a clear NOT_IMPLEMENTED when cuDNN is
absent unless there is product demand.
For each Phase‑3 op, introduce a dispatch at ComputeInternal:
if info.enable_cudnn and CudnnLibrary::Get().Available() and (cuDNN path preferred): use cuDNN
else: use native CUDA / CUTLASS / Triton-cubin fallback
This preserves cuDNN performance where present while removing the hard requirement.
Triton cubins (see docs/ORT_Use_Triton_Kernel.md) are an option for fused/normalization
kernels; CUTLASS (already vendored) for conv/GEMM‑shaped work.
NOT_IMPLEMENTED).enable_cudnn=0, the op uses the native fallback and does not load cuDNN.CudnnLibrary (e.g. an internal
SetForceUnavailableForTest(true) compiled only in test/internal builds), avoiding the
need to physically remove cuDNN in CI.enable_cudnn=0; this should not touch
the dynamic loader at all.NOT_IMPLEMENTED error in the
forced‑absent mode (Phase 1), and correctness in present mode.enable_cudnn=0 fails before
cudnn_frontend attempts to resolve backend symbols.ldd / readelf -d on
Linux, dumpbin /dependents on Windows) and confirm there is no direct dependency on
libcudnn* / cudnn64_*.dll even though cudnn_frontend headers are compiled in.enable_cudnn=0 and with the forced‑absent loader hook. These should pass, not throw.enable_cudnn=0 / forced‑absent coverage for that op.NOT_IMPLEMENTED message. Add the Windows
equivalent after the Windows cuDNN sub-DLL loading path is covered.onnxruntime.preload_dlls(cudnn=True, directory=...)
still preloads a user-provided cuDNN directory, while normal import onnxruntime and CUDA
EP initialization do not fail or print install guidance solely because cuDNN is missing.cudnn_symbols.inc would cause an unresolved-symbol link error or a runtime trampoline
failure. Mitigation: the probe-binary diff in §4.1-1, and CI that builds with cuDNN
import libs unavailable.cudnn_frontend dynamic-loading integration. If
NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING is not applied consistently, frontend graph code may
still create imports for cudnnBackend* symbols. If cudnn_frontend::cudnn_dlhandle is not
defined/set by ORT, frontend calls will throw while resolving backend symbols. Mitigation:
apply the compile definition to every target that includes frontend headers, define the
global handle once in ORT, and add binary dependency plus Conv/ConvTranspose frontend smoke
tests.cudnn_frontend may add new backend symbols across dependency bumps. A future
cudnn_frontend version may resolve additional backend, CUDA, CUDART, NVRTC, or experimental
symbols. Mitigation: after every cudnn_frontend update, grep/audit
cudnn_frontend_shim.h and experimental shims, then update the frontend-symbol audit tests.onnxruntime.preload_dlls(cudnn=True, directory=...)
from application code before creating the session; C++ deployments should use container,
package-manager, or system loader configuration.cudnn_path provider
option. Keep custom library discovery at trusted process/deployment layers instead.onnxruntime.preload_dlls(cudnn=True) tries to load cuDNN and prints installation guidance
on failure. That is useful when the user requested preloading, but too alarming if cuDNN is
optional. Mitigation: keep explicit preloading available, but avoid invoking or requiring
cuDNN preload as part of normal optional-cuDNN package import / CUDA EP load. Update docs so
users who need a specific cuDNN directory can call preload_dlls(cudnn=True, directory=...)
before creating the session.cmake/onnxruntime_python.cmake fails if no cudnn64_*.dll is found when generating
version_info.py. Mitigation: make cudnn_version optional in that generated file.RTLD_LOCAL); document the constraint; consider a build option that keeps the
classic hard‑link behavior for TRT‑combined packages.cudnnGetVersion in the loader and refuse (mark unavailable) on incompatible major
versions.cudnn_frontend headers) to compile; it no
longer requires cuDNN import libraries to link the in‑scope targets.cudnn_frontend remains a compile-time dependency while ORT uses frontend graph APIs for
Conv / ConvTranspose. It should not introduce a runtime cuDNN dependency when compiled with
NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING and wired to ORT's loader handle.onnxruntime.preload_dlls() remains supported for users who want Python to preload CUDA /
cuDNN libraries from PyTorch, NVIDIA site packages, or an explicit directory. It becomes an
optional convenience path for cuDNN, not a requirement for importing ORT or initializing the
CUDA EP without cuDNN.CudaContext::cudnn_handle
may now be nullptr; this is documented, and FetchResource returns null gracefully.enable_cudnn=0.preload_dlls(cudnn=True, directory=...)
from application code before creating the session.USE_CUDA_MINIMAL build-time path. It is used by RTX/TensorRT-related EP
builds and is not replaced by the CUDA EP runtime shim.