NVIDIA製ライブラリ関連のOpenCV CMakeオプションパーフェクトガイド

この記事は[OpenCV Advent Calendar 2024](https://qiita.com/advent-calendar/2024/opencv)の3日目の記事です。

## はじめに

OpenCVの一部モジュールはNVIDIA製ライブラリを用いることができ、CMakeオプションによって有効・無効などを制御することができます。一応、[OpenCV公式ドキュメント](åhttps://docs.opencv.org/4.10.0/db/d05/tutorial_config_reference.html#autotoc_md897)にも情報があるのですが、一部の情報しか載っていません。そのため、本記事ではOpenCVのNVIDIA製ライブラリ関連CMakeオプションについて詳しく解説します。  
※本記事の内容は[OpenCV 4.10.0](https://github.com/opencv/opencv/releases/tag/4.10.0)時点のソースコードをもとに書いています。

## NVIDIA製ライブラリ関連CMakeオプション

OpenCVにおけるNVIDIA製ライブラリ関連CMakeオプションを下表にまとめます。以降でこのうちいくつかピックアップして詳しく解説します。

|CMakeオプション|意味|
|---|---|
|BUILD_CUDA_STUBS|OpenCVのcudaモジュールをCUDA SDKなしの環境でビルドするためのスタブをビルドするか|
|ENABLE_CUDA_FIRST_CLASS_LANGUAGE|CUDA Toolkit検出時にCMakeの`enable_language(CUDA)`を使うかどうか|
|OPENCV_DNN_CUDA|OpenCVのdnnモジュールでCUDA実装を有効にするか|
|OPENCV_CMAKE_CUDA_DEBUG|OpenCVにおけるCUDA関連のCMake処理のログを詳細に表示する（OpenCV開発者向け）|
|WITH_CUDA|OpenCVで[CUDA](https://developer.nvidia.com/cuda-toolkit)実装を有効にするか|
|WITH_CUDNN|OpenCVで[cuDNN](https://developer.nvidia.com/cudnn)実装を有効にする|
|WITH_CUFFT|OpenCVで[cuFFT](https://docs.nvidia.com/cuda/cufft/)実装を有効にするか|
|WITH_CUBLAS|OpenCVで[cuBLAS](https://developer.nvidia.com/cublas)実装を有効にするか|
|WITH_NVCUVID|OpenCVで[NVIDIA Video Decoder(NVCUVID)](https://docs.nvidia.com/cuda/video-decoder/index.html)実装を有効にするか|
|WITH_NVCUVENC|OpenCVで[NVIDIA Video Codec SDK](https://developer.nvidia.com/video-codec-sdk)実装を有効にするか|
|CUDA_GENERATION|どのGPU世代向けのバイナリを生成するか|
|CUDA_ARCH_PTX|ターゲットのPTXアーキテクチャを指定|
|CUDA_ARCH_BIN|ターゲットのGPUアーキテクチャを指定|
|CUDA_ARCH_FEATURES|CUDA featureを指定|
|CMAKE_CUDA_ARCHITECTURES|どのNVIDIA GPUアーキテクチャ向けにビルドするかを指定|

### ENABLE_CUDA_FIRST_CLASS_LANGUAGE

`ENABLE_CUDA_FIRST_CLASS_LANGUAGE=ON`の場合、OpenCVビルド時にCUDAを参照する際に`enable_language(CUDA)`が使われるようになります。CMakeの`enable_language(CUDA)`について詳細を知りたい方は<https://developer.download.nvidia.com/video/gputechconf/gtc/2019/presentation/s9444-build-systems-exploring-modern-cmake-cuda-v2.pdf>を読むとよいでしょう。

#### このオプションが出来た経緯

このオプションができた経緯について少し補足します。CMakeドキュメントの<https://cmake.org/cmake/help/v3.31/module/FindCUDA.html>に以下の記述があります。

> It is no longer necessary to use this module or call `find_package(CUDA)` for compiling CUDA code. Instead, list CUDA among the languages named in the top-level call to the [project()](https://cmake.org/cmake/help/v3.31/command/project.html#command:project) command, or call the [enable_language()](https://cmake.org/cmake/help/v3.31/command/enable_language.html#command:enable_language) command with `CUDA`. Then one can add CUDA (`.cu`) sources directly to targets similar to other languages.
> 
> *Added in version 3.17:* To find and use the CUDA toolkit libraries manually, use the [FindCUDAToolkit](https://cmake.org/cmake/help/v3.31/module/FindCUDAToolkit.html#module:FindCUDAToolkit) module instead. It works regardless of the CUDA language being enabled.

従来、CMakeでCUDA Toolkitを検出する際、`find_package(CUDA)`が使われていましたが、

> Deprecated since version 3.10: Do not use this module in new code.

とあり、この記法はCMake 3.10以降で推奨されていません。そのため、<https://cliutils.gitlab.io/modern-cmake/chapters/packages/CUDA.html>にあるように

```cmake
enable_language(CUDA)
```

などに移行することが推奨されています。このような背景から`ENABLE_CUDA_FIRST_CLASS_LANGUAGE`が追加されています。

#### ENABLE_CUDA_FIRST_CLASS_LANGUAGEについてもう少し掘り下げる

OpenCV内部でどうなっているかを知るために<https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVFindLibsPerf.cmake#L41-L54>を読んでみます。

```cmake
# --- CUDA ---
if(WITH_CUDA)
  if(ENABLE_CUDA_FIRST_CLASS_LANGUAGE)
    include("${OpenCV_SOURCE_DIR}/cmake/OpenCVDetectCUDALanguage.cmake")
  else()
    include("${OpenCV_SOURCE_DIR}/cmake/OpenCVDetectCUDA.cmake")
  endif()
  if(NOT HAVE_CUDA)
    message(WARNING "OpenCV is not able to find/configure CUDA SDK (required by WITH_CUDA).
CUDA support will be disabled in OpenCV build.
To eliminate this warning remove WITH_CUDA=ON CMake configuration option.
")
  endif()
endif(WITH_CUDA)
```

上記ファイルを読むとわかるように`ENABLE_CUDA_FIRST_CLASS_LANGUAGE`の値に応じてCMakeファイルを読み分けていることがわかります。

|条件|CMakeファイル|
|---|---|
|ENABLE_CUDA_FIRST_CLASS_LANGUAGE=OFF|[cmake/OpenCVDetectCUDA.cmake](https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDA.cmake)|
|ENABLE_CUDA_FIRST_CLASS_LANGUAGE=ON|[cmake/OpenCVDetectCUDALanguage.cmake](https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDALanguage.cmake)|

### CUDA_GENERATION

`CUDA_GENERATION`オプションによってどの世代のNVIDIA GPU向けにビルドするかを指定することができます。例えば、筆者が持っているGeforce RTX 2080 Ti（Turing世代）だと以下のような指定になります。


```bash
-D CUDA_GENERATION=Turing
```

#### CUDA_GENERATIONについてもう少し掘り下げる

`CUDA_GENERATION`を指定した時のOpenCVのCMake周りの処理をもう少し追ってみましょう。[opencv/cmake/OpenCVDetectCUDAUtils.cmake](https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDAUtils.cmake)に以下の記述があります。

```cmake
macro(ocv_set_cuda_arch_bin_and_ptx nvcc_executable)
  ocv_initialize_nvidia_device_generations()
  set(__cuda_arch_ptx ${CUDA_ARCH_PTX})
  # 中略
  elseif(CUDA_GENERATION STREQUAL "Turing")
    set(__cuda_arch_bin ${_arch_turing})
  # 中略
```

例えば、`CUDA_GENERATION=Turing`の場合、`set(__cuda_arch_bin ${_arch_turing})`が実行されます。[opencv/cmake/OpenCVDetectCUDAUtils.cmake](https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDAUtils.cmake)では`_arch_turing`の定義は以下のようになっており、結果として__cuda_arch_binに`7.5`が設定されます。

```cmake
set(_arch_turing   "7.5")
```

また、`CUDA_GENERATION`には`Auto`を設定することもできます。この場合、どのような挙動になるのでしょうか？挙動を理解するために[opencv/cmake/OpenCVDetectCUDAUtils.cmake](https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDAUtils.cmake)を読んでいきましょう。Autoの場合、`ocv_detect_native_cuda_arch`マクロが呼ばれます。

```cmake
elseif(CUDA_GENERATION STREQUAL "Auto")
  ocv_detect_native_cuda_arch(${nvcc_executable} _nvcc_res _nvcc_out)
  if(NOT _nvcc_res EQUAL 0)
    message(STATUS "CUDA: Automatic detection of CUDA generation failed. Going to build for all known architectures")
  else()
    string(REGEX MATCHALL "[0-9]+\\.[0-9]" __cuda_arch_bin "${_nvcc_out}")
  endif()
```

`ocv_detect_native_cuda_arch`マクロの中身をさらに追っていきます。

```cmake
macro(ocv_detect_native_cuda_arch nvcc_executable status output)
  set(OPENCV_CUDA_DETECT_ARCHS_COMMAND "${nvcc_executable}" ${OPENCV_CUDA_DETECTION_NVCC_FLAGS} "${OpenCV_SOURCE_DIR}/cmake/checks/OpenCVDetectCudaArch.cu" "--run")
  set(__cache_key_check "${OPENCV_CUDA_DETECT_ARCHS_COMMAND}")
  if(DEFINED OPENCV_CACHE_CUDA_ACTIVE_CC AND OPENCV_CACHE_CUDA_ACTIVE_CC_check STREQUAL __cache_key_check)
    set(${output} "${OPENCV_CACHE_CUDA_ACTIVE_CC}")
    set(${status} 0)
  else()
    execute_process(
        COMMAND ${OPENCV_CUDA_DETECT_ARCHS_COMMAND}
        WORKING_DIRECTORY "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/"
        RESULT_VARIABLE ${status}
        OUTPUT_VARIABLE _nvcc_out
        ERROR_VARIABLE _nvcc_err
        ERROR_QUIET
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    if(OPENCV_CMAKE_CUDA_DEBUG)
      message(WARNING "COMMAND: ${OPENCV_CUDA_DETECT_ARCHS_COMMAND}")
      message(STATUS "Result: ${${status}}")
      message(STATUS "Out: ${_nvcc_out}")
      message(STATUS "Err: ${_nvcc_err}")
    endif()
    string(REGEX REPLACE ".*\n" "" ${output} "${_nvcc_out}") #Strip leading warning messages, if any

    if(${status} EQUAL 0)
      # cache detected values
      set(OPENCV_CACHE_CUDA_ACTIVE_CC ${${output}} CACHE INTERNAL "")
      set(OPENCV_CACHE_CUDA_ACTIVE_CC_check "${__cache_key_check}" CACHE INTERNAL "")
    endif()
  endif()
endmacro()
```

おおまかには以下のことを行っています。

1. nvccで[opencv/cmake/checks/OpenCVDetectCudaArch.cu](https://github.com/opencv/opencv/blob/4.10.0/cmake/checks/OpenCVDetectCudaArch.cu)をビルドする
1. execute_process()によってビルドしたプログラムを実行する
1. プログラムの実行結果をもとに実行環境のGPU情報を取得する

さらに掘り下げるために[opencv/cmake/checks/OpenCVDetectCudaArch.cu](https://github.com/opencv/opencv/blob/4.10.0/cmake/checks/OpenCVDetectCudaArch.cu)を読んでみましょう。

```c++
#include <iostream>
#include <sstream>
#include <list>

int main()
{
    std::ostringstream arch;
    std::list<std::string> archs;

    int count = 0;
    if (cudaSuccess != cudaGetDeviceCount(&count)){ return -1; }
    if (count == 0) { return -1; }
    for (int device = 0; device < count; ++device)
    {
        cudaDeviceProp prop;
        if (cudaSuccess != cudaGetDeviceProperties(&prop, device)){ continue; }
        arch << prop.major << "." << prop.minor;
        archs.push_back(arch.str());
        arch.str("");
    }
    archs.unique(); // Some devices might have the same arch
    for (std::list<std::string>::iterator it=archs.begin(); it!=archs.end(); ++it)
        std::cout << *it << " ";
    return 0;
}
```

このプログラムではおおまかには以下のようなことをやっています。

1. [cudaGetDeviceCount()](https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__DEVICE.html#group__CUDART__DEVICE_1g18808e54893cfcaafefeab31a73cc55f)で実行環境のNVIDIA GPU台数をチェックする
1. 実行環境のNVIDIA GPUごとに[cudaGetDeviceProperties()](https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__DEVICE.html#group__CUDART__DEVICE_1g1bf9d625a931d657e08db2b4391170f0)でプロパティを取得する
1. プロパティからアーキテクチャのバージョン情報を取り出す

### CUDA_ARCH_PTX、CUDA_ARCH_BIN

- `CUDA_ARCH_PTX`オプションでターゲットのPTXアーキテクチャを指定することができます
- `CUDA_ARCH_BIN`オプションでターゲットのGPUアーキテクチャを指定することができます

Compute Capabilityについては<https://zenn.dev/eduidl/articles/cuda-comparability>を参照ください。例えば、筆者が持っているGeforce RTX 2080 Tiを例に挙げると、このGPUのCompute Capabilityは7.5になっています（<https://developer.nvidia.com/cuda-gpus>参照）。例えば、ターゲットのPTXアーキテクチャ、GPUアーキテクチャともにCompute Capabilityを7.5に指定する場合、以下のようなCMakeオプション指定になります。

```bash
-D CUDA_ARCH_PTX=7.5 -D CUDA_ARCH_BIN=7.5
```

### CUDA_ARCH_FEATURES

`CUDA_ARCH_FEATURES`オプションでCUDA featureを指定することができます。指定できる値は<https://cmake.org/cmake/help/v3.31/prop_gbl/CMAKE_CUDA_KNOWN_FEATURES.html#prop_gbl:CMAKE_CUDA_KNOWN_FEATURES>を参照してください。`CUDA/C++17`の場合、以下のようなCMakeオプション指定になります。
```bash
-D CMAKE_CUDA_COMPILE_FEATURES=cuda_std_17
```

CMakeにおける`CUDA_ARCH_FEATURES`について詳細を知りたい方はあわせてこちらを読むとよいでしょう。

- <https://cmake.org/cmake/help/v3.31/variable/CMAKE_CUDA_COMPILE_FEATURES.html#variable:CMAKE_CUDA_COMPILE_FEATURES>
- <https://cmake.org/cmake/help/v3.31/prop_gbl/CMAKE_CUDA_KNOWN_FEATURES.html#prop_gbl:CMAKE_CUDA_KNOWN_FEATURES>
- <https://developer.nvidia.com/blog/building-cuda-applications-cmake/>

### OPENCV_CMAKE_FORCE_CUDA

コンパイル時にOpenCVにおけるCUDA実装のコンパイルを無効化するオプションになっています（FORCE_CUDAという名前から受ける印象とは逆の挙動になっているのが気になります・・・）。

```cmake
if((NOT UNIX AND CV_CLANG) OR OPENCV_CMAKE_FORCE_CUDA)
  message(STATUS "CUDA: Compilation is disabled (due to Clang unsupported on your platform).")
  return()
endif()
```

具体的には以下のCMakeファイルで使用されます。

|条件|CMakeファイル|
|---|---|
|ENABLE_CUDA_FIRST_CLASS_LANGUAGE=OFF|[cmake/OpenCVDetectCUDA.cmake](https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDA.cmake)|
|ENABLE_CUDA_FIRST_CLASS_LANGUAGE=ON|[cmake/OpenCVDetectCUDALanguage.cmake](https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDALanguage.cmake)|

### CMAKE_CUDA_ARCHITECTURES

CMAKE_CUDA_ARCHITECTURESはCMake本体で定義されているもので、<https://cmake.org/cmake/help/v3.31/variable/CMAKE_CUDA_ARCHITECTURES.html>にあるようにどのNVIDIA GPUアーキテクチャ向けにビルドするかを指定できます。例えば、筆者が持っているGeforce RTX 2080 TiのGPUのCompute Capabilityは7.5になっています（<https://developer.nvidia.com/cuda-gpus>参照）。このデバイス（Compute Capability 7.5）向けにビルドする場合の指定方法は以下の通りです。この場合、`7.5`ではなく、`75`となる点に注意が必要です。

```bash
-D CMAKE_CUDA_ARCHITECTURES=75
```

### その他

#### NVCC_FLAGS_EXTRA

`NVCC_FLAGS_EXTRA`によってNVCCによるコンパイル時に使用するフラグ追加することができます。

- <https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDA.cmake#L79>
- <https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDALanguage.cmake#L81>

デフォルトだとNVCC_FLAGS_EXTRAの中身は空になっています。

```cmake
# NVCC flags to be set
set(NVCC_FLAGS_EXTRA "")
```

例えば、以下のように変更してOpenCVをビルドすることでCUDAカーネルのデバッグができるようになります。

```cmake
# NVCC flags to be set
set(NVCC_FLAGS_EXTRA "-G -g")
```

## 各種ライブラリはOpenCVのどのモジュールで使われているのか？

OpenCVでcuDNN、cuFFT、cuBLASが使われることは知っていても具体的にどのモジュールで使われているかまでは知らないという方が多いのではないでしょうか？ここではどのモジュールのどの処理で使われているかをまとめました。

### cuDNN

- dnnモジュールで使用
  - <https://github.com/opencv/opencv/tree/4.10.0/modules/dnn>

### cuFFT

- cudaarithmモジュールで使用
  - `cv::cuda::createDFT`、`cv::cuda::createConvolution`でcuBLASが呼び出される
    - <https://github.com/opencv/opencv_contrib/blob/4.10.0/modules/cudaarithm/src/precomp.hpp#L59-L61>
    - <https://github.com/opencv/opencv_contrib/blob/4.10.0/modules/cudaarithm/src/arithm.cpp#L303-L418>
    - <https://github.com/opencv/opencv_contrib/blob/4.10.0/modules/cudaarithm/src/arithm.cpp#L420-L430>
    - <https://github.com/opencv/opencv_contrib/blob/4.10.0/modules/cudaarithm/src/arithm.cpp#L435-L587>
    - <https://github.com/opencv/opencv_contrib/blob/4.10.0/modules/cudaarithm/src/arithm.cpp#L589-L598>

### cuBLAS

- cudaarithmモジュールで使用
  - `cv::cuda::gemm`でcuBLASが呼び出される
    - <https://github.com/opencv/opencv_contrib/blob/4.10.0/modules/cudaarithm/src/precomp.hpp#L55-L57>
    - <https://github.com/opencv/opencv_contrib/blob/4.10.0/modules/cudaarithm/src/arithm.cpp#L157-L286>
- dnnモジュールで使用
  - <https://github.com/opencv/opencv/tree/4.10.0/modules/dnn>

### NVCUVID

- cudacodecモジュールで使用
  - <https://github.com/opencv/opencv_contrib/tree/4.10.0/modules/cudacodec>
  - 動画像のデコード処理で使用

### NVCUVENC

- cudacodecモジュールで使用
  - <https://github.com/opencv/opencv_contrib/tree/4.10.0/modules/cudacodec>
  - 動画像のエンコード処理で使用

### NVIDIA Optical Flow SDK

- cudaoptflowモジュールで使用
  - <https://github.com/opencv/opencv_contrib/blob/4.10.0/modules/cudaoptflow/src/nvidiaOpticalFlow.cpp>
  - <https://github.com/opencv/opencv_contrib/blob/4.10.0/modules/cudaoptflow/CMakeLists.txt>
  - 詳細は[OpenCVのcudaoptflowモジュールからNVIDIA Optical Flow SDKを使う](https://qiita.com/dandelion1124/items/ebe51b683ed4f285a4b1)参照。

## 注意点

<https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVDetectCUDAUtils.cmake#L61>に以下の説明があります。

> Use CMAKE_CUDA_ARCHITECTURES if provided: order of preference CMAKE_CUDA_ARCHITECTURES > CUDA_GENERATION > CUDA_ARCH_BIN and/or CUDA_ARCH_PTX

ただし、この優先順位が守られないバグが<https://github.com/opencv/opencv/issues/25920>で報告されており、<https://github.com/opencv/opencv/pull/25941>で修正されているようです。

## 宣伝

関連情報として、OpenCVでのCUDA（GpuMat）活用については筆者のスライド<https://www.docswell.com/s/fixstars/ZRXQ72-20220805>もあわせて参照ください。

## おわりに

本記事ではOpenCVのNVIDIA製ライブラリ関連CMakeオプションについて解説しました。
今回の解説でOpenCVをより使いこなしてくれる方が増えると幸いです。
明日は，@ASAKA-219 さんの[OpenCVとROS 2で学ぶ画像処理入門](https://qiita.com/ASAKA-219/items/ec17eaf4a7f4cc104dc6)です！