Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions .github/workflows/linux_cuda_no_cudnn.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,15 @@ jobs:
done < perms.txt
fi

- name: Verify CUDA provider has no direct cuDNN dependency
- name: Verify CUDA provider has no direct cuDNN or cuFFT dependency
run: |
docker run --rm --gpus all \
-v "${{ runner.temp }}/Release:/build/Release" \
"${{ steps.build_docker_image_step.outputs.full-image-name }}" \
bash -lc 'set -euo pipefail
ldd /build/Release/Release/libonnxruntime_providers_cuda.so | tee /tmp/ldd.txt
! grep -i cudnn /tmp/ldd.txt'
! grep -i cudnn /tmp/ldd.txt
! grep -i cufft /tmp/ldd.txt'

- name: Run no-cuDNN CUDA EP op smoke test
run: |
Expand Down Expand Up @@ -137,6 +138,24 @@ jobs:
exit 1
fi

# Likewise remove cuFFT so the smoke test proves the provider loads and runs
# non-FFT operators when cuFFT is physically unavailable (it is loaded lazily and
# only needed by the Rfft/Irfft contrib ops).
for root in /usr /opt /lib /lib64; do
if [ -e "$root" ]; then
find "$root" -name "libcufft*.so*" -print -delete 2>/dev/null || true
fi
done
ldconfig
if ldconfig -p | grep -qi cufft; then
echo "cuFFT remains available in the dynamic linker cache" >&2
exit 1
fi
if find /usr /opt /lib /lib64 -name "libcufft*.so*" -print -quit 2>/dev/null | grep -q .; then
echo "cuFFT runtime libraries remain in the no-cuDNN test container" >&2
exit 1
fi

WHEEL_PATH=$(find /build/Release/Release/dist -type f -name "onnxruntime_gpu-*.whl" | head -n 1)
if [ -z "$WHEEL_PATH" ]; then
echo "No built onnxruntime GPU wheel found under /build/Release/Release/dist" >&2
Expand Down Expand Up @@ -255,5 +274,32 @@ jobs:
np.argmin(data, axis=1).reshape(2, 1).astype(np.int64),
)

print("CUDA no-cuDNN op smoke tests passed")
# Rfft requires cuFFT, which was physically removed above. With CPU fallback
# disabled the node must run on CUDA and fail cleanly with NOT_IMPLEMENTED
# (proving cuFFT is optional and its absence does not crash the provider).
rfft_model = helper.make_model(
helper.make_graph(
[helper.make_node("Rfft", ["x"], ["y"], domain="com.microsoft", signal_ndim=1, onesided=1)],
"no_cufft_rfft",
[helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 4])],
[helper.make_tensor_value_info("y", TensorProto.FLOAT, [2, 3, 2])],
),
opset_imports=[helper.make_opsetid("", 17), helper.make_opsetid("com.microsoft", 1)],
)
rfft_model.ir_version = 10
rfft_options = ort.SessionOptions()
rfft_options.add_session_config_entry("session.disable_cpu_ep_fallback", "1")
try:
rfft_sess = ort.InferenceSession(
rfft_model.SerializeToString(), sess_options=rfft_options, providers=providers
)
rfft_sess.run(None, {"x": np.random.rand(2, 4).astype(np.float32)})
except Exception as exc: # noqa: BLE001
message = str(exc).lower()
assert "cufft" in message, "Rfft without cuFFT raised an unexpected error: " + str(exc)
print("[no-cuFFT] Rfft correctly failed: " + str(exc).splitlines()[0])
else:
raise AssertionError("Rfft unexpectedly succeeded while cuFFT was unavailable")

print("CUDA no-cuDNN/no-cuFFT op smoke tests passed")
PY'
18 changes: 16 additions & 2 deletions .github/workflows/windows_cuda_no_cudnn.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,20 @@ jobs:
dir
shell: pwsh

- name: Remove cuFFT runtime DLLs
shell: pwsh
run: |
$cudaRoot = Join-Path $env:RUNNER_TEMP "v13.0"
Get-ChildItem -Path $cudaRoot -Recurse -Include "cufft*.dll" | Remove-Item -Force
if (Get-ChildItem -Path $cudaRoot -Recurse -Include "cufft*.dll" -ErrorAction SilentlyContinue) {
Write-Error "cuFFT runtime DLLs must not be present in the no-cuDNN test environment"
exit 1
}

- name: Add CUDA to PATH
shell: pwsh
run: |
Write-Host "Adding CUDA to PATH without adding any cuDNN directory"
Write-Host "Adding CUDA to PATH without cuDNN or cuFFT runtime DLLs"
Add-Content -Path $env:GITHUB_PATH -Value "$env:RUNNER_TEMP\v13.0\bin"
Add-Content -Path $env:GITHUB_PATH -Value "$env:RUNNER_TEMP\v13.0\bin\x64"
Add-Content -Path $env:GITHUB_PATH -Value "$env:RUNNER_TEMP\v13.0\extras\CUPTI\lib64"
Expand All @@ -237,7 +247,7 @@ jobs:
shell: pwsh
run: nvidia-smi

- name: Verify CUDA plugin has no direct cuDNN dependency
- name: Verify CUDA plugin has no direct cuDNN or cuFFT dependency
shell: pwsh
run: |
$pluginPath = "${{ runner.temp }}\build\Release\Release\onnxruntime_providers_cuda.dll"
Expand All @@ -251,6 +261,10 @@ jobs:
Write-Error "CUDA plugin EP has a direct cuDNN dependency"
exit 1
}
if (Select-String -Path $env:RUNNER_TEMP\cuda_plugin_dependents.txt -Pattern "cufft" -SimpleMatch -Quiet) {
Write-Error "CUDA plugin EP has a direct cuFFT dependency"
exit 1
}

- name: Run CUDA Plugin EP Python Tests without cuDNN
working-directory: ${{ github.workspace }}\onnxruntime\test\python\transformers
Expand Down
4 changes: 3 additions & 1 deletion cmake/onnxruntime_providers_cuda.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,9 @@
endif()
target_compile_definitions(${target} PRIVATE NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING)
target_include_directories(${target} PRIVATE ${CUDNN_INCLUDE_DIR})
target_link_libraries(${target} PRIVATE CUDA::cublasLt CUDA::cublas cudnn_frontend CUDA::curand CUDA::cufft CUDA::cudart CUDA::cuda_driver
# cuDNN and cuFFT are loaded dynamically at runtime (see cudnn_stub.cc / cufft_stub.cc), so they are
# intentionally not linked here to avoid a hard runtime dependency when the related ops are not used.
target_link_libraries(${target} PRIVATE CUDA::cublasLt CUDA::cublas cudnn_frontend CUDA::curand CUDA::cudart CUDA::cuda_driver
${ABSEIL_LIBS} ${ONNXRUNTIME_PROVIDERS_SHARED} Boost::mp11 safeint_interface)
endif()

Expand Down
4 changes: 3 additions & 1 deletion cmake/onnxruntime_providers_cuda_plugin.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,13 @@ onnxruntime_add_include_to_target(
add_dependencies(onnxruntime_providers_cuda_plugin ${onnxruntime_EXTERNAL_DEPENDENCIES})

# Link libraries
# cuDNN and cuFFT are loaded dynamically at runtime (see cudnn_stub.cc / cufft_stub.cc, which are
# picked up by the GLOB above), so they are intentionally not linked here to avoid a hard runtime
# dependency when the related ops are not used.
target_link_libraries(onnxruntime_providers_cuda_plugin PRIVATE
CUDA::cudart
CUDA::cublas
CUDA::cublasLt
CUDA::cufft
CUDA::cuda_driver
cudnn_frontend
Boost::mp11
Expand Down
2 changes: 1 addition & 1 deletion cmake/onnxruntime_unittests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,7 @@ if (onnxruntime_ENABLE_CUDA_EP_INTERNAL_TESTS AND onnxruntime_BUILD_CUDA_EP_AS_P
"${ONNXRUNTIME_ROOT}/core/providers/cuda/cuda_utils.cu"
"${ONNXRUNTIME_ROOT}/core/providers/cuda/cudnn_common.cc"
"${ONNXRUNTIME_ROOT}/core/providers/cuda/cudnn_loader.cc"
"${ONNXRUNTIME_ROOT}/core/providers/cuda/cufft_loader.cc"
"${ONNXRUNTIME_ROOT}/core/providers/cuda/reduction/reduction_functions.cc"
"${ONNXRUNTIME_ROOT}/core/providers/cuda/reduction/reduction_functions.cu"
"${TEST_SRC_DIR}/providers/cuda/test_cases/cuda_plugin_test_shims.cc"
Expand Down Expand Up @@ -1408,7 +1409,6 @@ block()
CUDA::cublas
CUDA::cublasLt
CUDA::curand
CUDA::cufft
CUDA::cuda_driver
CUDNN::cudnn
cudnn_frontend)
Expand Down
34 changes: 34 additions & 0 deletions docs/CUDA_cuDNN_Optional_Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,40 @@ TensorRT / NV‑RTX EPs are untouched and continue to link cuDNN as before. (If
and the shimmed CUDA EP are in the same process, symbol collision must be avoided — see
§8 Risks.)

### 3.6 Applying the same pattern to cuFFT

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:

- **Loader** — `onnxruntime/core/providers/cuda/cufft_loader.{h,cc}` defines a
`CufftLibrary` singleton that mirrors `CudnnLibrary`: it lazily `dlopen`/`LoadLibrary`s 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.
- **Shim** — `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**.
- **Availability guard** — `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.

---

## 4. Phase 1 — Make cuDNN optional (no new kernels)
Expand Down
8 changes: 7 additions & 1 deletion docs/cuda_plugin_ep/cuda_plugin_ep_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ There is intentionally no provider option for a custom cuDNN DLL/SO path. Provid

No-cuDNN plugin validation runs `test_cuda_plugin_ep.py` with `ORT_TEST_CUDA_PLUGIN_EP=1` and `ORT_TEST_CUDA_PLUGIN_NO_CUDNN=1`. That mode passes `enable_cudnn=0` to plugin sessions and skips tests for operators that still require cuDNN in the current implementation. Non-cuDNN operator coverage, plugin registration, device enumeration, graph assignment, CUDA graph, I/O binding, and profiling tests continue to run.

### 2.1.2 Optional cuFFT Runtime Dependency

The CUDA Plugin EP applies the same optional-dependency model to **cuFFT**. cuFFT is only used by the FFT contrib operators (`Rfft` / `Irfft`), so the plugin shared library must not link directly to cuFFT or contain a cuFFT DLL/SO in its dynamic dependency table. cuFFT is loaded lazily through the ORT cuFFT loader (`CufftLibrary`, `core/providers/cuda/cufft_loader.{h,cc}`) on first FFT-op use; the entry points ORT calls are provided by `cufft_stub.cc` trampolines that forward to the resolved symbols.

Unlike cuDNN, there is **no provider option** for cuFFT (no `enable_cufft`) and no frontend library to configure. When cuFFT is unavailable, `CUFFT_RETURN_IF_ERROR` (and `CudaCall<cufftResult>`) return `NOT_IMPLEMENTED` with an actionable message, so FFT ops fail cleanly while all other operators keep working. cuFFT headers (part of the CUDA toolkit) are still required at build time, but `CUDA::cufft` is no longer linked. The loader picks the cuFFT library name at compile time from the `CUDA_VERSION` macro (cuFFT 12 for CUDA 13.x, cuFFT 11 for CUDA 12.x); unsupported CUDA major versions fail at compile time until their mapping is added.

### 2.2 Preprocessor Defines

Each build target uses different preprocessor defines that control how framework types are resolved:
Expand Down Expand Up @@ -651,7 +657,7 @@ Use `onnxruntime_BUILD_CUDA_EP_AS_PLUGIN=OFF` when you need to build the origina

### 9.3 Plugin Independence

The plugin build's `libonnxruntime_providers_cuda.so` is **fully self-contained**. It does not depend on `libonnxruntime_providers_shared.so` at load time. It statically links against `onnxruntime_framework`, `onnxruntime_graph`, `onnxruntime_common`, `onnxruntime_mlas`, `onnxruntime_flatbuffers`, and links dynamically against CUDA (`cudart`, `cublas`, `cublasLt`, `cufft`) and protobuf. cuDNN is loaded lazily only when enabled and available at runtime. Communication with the ORT runtime happens exclusively through the C API (`OrtApi`/`OrtEpApi`) passed at load time.
The plugin build's `libonnxruntime_providers_cuda.so` is **fully self-contained**. It does not depend on `libonnxruntime_providers_shared.so` at load time. It statically links against `onnxruntime_framework`, `onnxruntime_graph`, `onnxruntime_common`, `onnxruntime_mlas`, `onnxruntime_flatbuffers`, and links dynamically against CUDA (`cudart`, `cublas`, `cublasLt`) and protobuf. cuDNN and cuFFT are loaded lazily only when available at runtime (cuDNN additionally gated by `enable_cudnn`), so neither appears in the library's dynamic dependency table. Communication with the ORT runtime happens exclusively through the C API (`OrtApi`/`OrtEpApi`) passed at load time.

### 9.4 Build Outputs

Expand Down
13 changes: 13 additions & 0 deletions onnxruntime/core/providers/cuda/cuda_call.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#endif
#ifndef USE_CUDA_MINIMAL
#include "core/providers/cuda/cudnn_loader.h"
#include "core/providers/cuda/cufft_loader.h"
#endif

#include <type_traits>
Expand Down Expand Up @@ -107,6 +108,18 @@ std::conditional_t<THRW, void, Status> CudaCall(
}
}
}
if constexpr (std::is_same_v<ERRTYPE, cufftResult>) {
if (!cuda::CufftLibrary::Get().Available()) {
auto status = ORT_MAKE_STATUS(ONNXRUNTIME, NOT_IMPLEMENTED,
"cuFFT is unavailable for CUDA Execution Provider: ",
cuda::CufftLibrary::Get().Error());
if constexpr (THRW) {
ORT_THROW(status.ErrorMessage());
} else {
return status;
}
}
}
#endif
try {
#ifdef _WIN32
Expand Down
Loading
Loading