syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

OpenCLデバイスの列挙

1年くらい前に書いたコードですが、OpenCLプログラムの入門、まずはデバイスの列挙です。
VC++ 2010、AMD APP SDK v2、NVIDIA GPU Computing SDK 4.0で検証。

#define __CL_ENABLE_EXCEPTIONS
#pragma warning(push)
#pragma warning(disable:4290)
#include <CL/cl.hpp>
#pragma warning(pop)
#include <iostream>
#include <conio.h>

#pragma comment(lib, "opencl.lib")

const char* CLHelloSrcCodeStr = "__kernel void hello(void) { }";

void ExecuteMySimpleOpenCLProgram(cl::Context& context)
{
  cl_int err = CL_SUCCESS;
  const std::vector<cl::Device> devices = context.getInfo<CL_CONTEXT_DEVICES>();

  cl::Program::Sources source(1, std::make_pair(CLHelloSrcCodeStr, strlen(CLHelloSrcCodeStr)));
  cl::Program program = cl::Program(context, source);
  program.build(devices);

  cl::Kernel kernel(program, "hello", &err);

  cl::Event event;
  cl::CommandQueue queue(context, devices[0], 0, &err);
  puts("Enqueue.");
  queue.enqueueNDRangeKernel(
    kernel, cl::NullRange, cl::NDRange(4, 4), cl::NullRange, nullptr, &event);
  puts("Waiting...");
  event.wait();
  puts("Finished.");
}

int main()
{
  cl_int err = CL_SUCCESS;
  try
  {
    std::vector<cl::Platform> platforms;
    cl::Platform::get(&platforms);
    if (platforms.empty())
    {
      std::cerr << "NO OpenCL Platform." << std::endl;
      return -1;
    }
    std::cout << "OpenCL Platform Count = " << platforms.size() << std::endl;

    for (auto it = platforms.begin(); it != platforms.end(); ++it)
    {
      // 何番目のプラットフォームがどのベンダーの GPU / CPU になるかは不定。
      // SDK やドライバーのインストール順序などにも左右される。
      std::string platformName;
      err = it->getInfo(CL_PLATFORM_NAME, &platformName);
      std::cout << "PlatformName = " << platformName << std::endl;
      // とりあえずすべての OpenCL デバイスを列挙させてみる。
      std::vector<cl::Device> devs;
      try
      {
        it->getDevices(CL_DEVICE_TYPE_ALL, &devs);
      }
      catch (const cl::Error&)
      {
        puts("NO OpenCL device in the platform!!");
        continue;
      }
      for (auto jt = devs.begin(); jt != devs.end(); ++jt)
      {
        std::string devName;
        err = jt->getInfo(CL_DEVICE_NAME, &devName);
        std::cout << "DeviceName = " << devName << std::endl;
      }

      // GPU デバイスの取得を試みて、取得できれば OpenCL プログラムを作成して実行させる。
      try
      {
        it->getDevices(CL_DEVICE_TYPE_GPU, &devs); // デバイスの取得に失敗したら例外が発生する。
      }
      catch (const cl::Error&)
      {
        puts("NO GPU device in the platform!!");
        devs.clear();
      }
      // 誤って余計な例外を捕捉しないよう、try-catch ブロックは分ける。
      if (!devs.empty())
      {
        cl_context_properties properties[] =
        {
          CL_CONTEXT_PLATFORM, reinterpret_cast<cl_context_properties>((*it)()),
          0
        };
        cl::Context context(CL_DEVICE_TYPE_GPU, properties);
        ExecuteMySimpleOpenCLProgram(context);
      }

      // CPU デバイスの取得を試みて、取得できれば OpenCL プログラムを作成して実行させる。
      try
      {
        it->getDevices(CL_DEVICE_TYPE_CPU, &devs); // デバイスの取得に失敗したら例外が発生する。
      }
      catch (const cl::Error&)
      {
        puts("NO CPU device in the platform!!");
        devs.clear();
      }
      // 誤って余計な例外を捕捉しないよう、try-catch ブロックは分ける。
      if (!devs.empty())
      {
        cl_context_properties properties[] =
        {
          CL_CONTEXT_PLATFORM, reinterpret_cast<cl_context_properties>((*it)()),
          0
        };
        cl::Context context(CL_DEVICE_TYPE_CPU, properties);
        ExecuteMySimpleOpenCLProgram(context);
      }
    }
  }
  catch (const cl::Error& errObj)
  {
    std::cerr << "ERROR: " << errObj.what() << "(" << errObj.err() << ")" << std::endl;
  }

  puts("Press any...");
  _getch();
  return 0;
}

OpenCLのハンドル管理設計はOpenGLよりはいくらかマシになっていて、GLintやGLuintみたいな単なる整数型ではなく、cl_contextとかはWin32 APIみたいにダミー構造体へのポインターになってます(ハンドル値を間違ったAPIに渡してしまうことを防止)。
OpenCLリソースはC言語関数の clCreateContext()/clReleaseContext() とかで明示的にリソース管理してもいいんですが、C++だったら cl.hpp に定義されてるRAIIラッパークラスを使いましょう。リソース管理だけでなくデバイス情報の取得もだいぶ楽になります。なお、エラーハンドリング(例外処理)に若干癖があるので注意。
OpenCL APIC++専用ラッパーが定義されてる <CL/cl.hpp> ヘッダーはKhronosの公式サイトからダウンロードできますが、Intel OpenCL SDKAMD APP SDKには付属してます。NVIDIA GPU Computing SDKには4.0時点で付属していないので、OpenCLヘッダーがある場所にKhronosからダウンロードしてきたファイルをコピーする必要があります。
例えばNV GPU SDK 4.0の場合は下記の場所にOpenCL関連のヘッダーとインポート ライブラリがインストールされているので、Visual Studioのグローバル インクルード ディレクトリとグローバル ライブラリ ディレクトリにはこの場所を追加指定してパスを通しておくとよいでしょう。

%ProgramData%\NVIDIA Corporation\NVIDIA GPU Computing SDK 4.0\OpenCL\common\inc\
%ProgramData%\NVIDIA Corporation\NVIDIA GPU Computing SDK 4.0\OpenCL\common\lib\

AMD APP SDKを使う場合はこちら。

%ProgramFiles(x86)%\AMD APP\include\
%ProgramFiles(x86)%\AMD APP\lib\

今のところPC向けにOpenCL SDKを公開している大手はIntelAMDNVIDIAの3社だけですかね。

実行例:

OpenCL Platform Count = 2
PlatformName = AMD Accelerated Parallel Processing
DeviceName = AMD Athlon(tm) 64 X2 Dual Core Processor 5600+
NO GPU device in the platform!!
Enqueue.
Waiting...
Finished.
PlatformName = NVIDIA CUDA
DeviceName = Quadro 600
Enqueue.
Waiting...
Finished.
NO CPU device in the platform!!
Press any...

テストに使ったデバイスがショボくて申し訳ないんですが、プラットフォームが列挙される順序は、OpenCL SDKOpenCL Driver)のインストール順に依存します。AMD APPプラットフォームは旧CPUに関してもSSE3をサポートしている世代であればOpenCLカーネルを実行できるようです。

ちなみにIntelのCPUが接続されていない状態でIntel OpenCL SDKIntel OpenCL Driver)をインストールするとどうなるんでしょう? プラットフォームだけ列挙されてデバイスが列挙されないんですかね?
AMDのCPU+GPUAMDのAPU、IntelのHD Graphics GPU内蔵Coreシリーズ、あとはマルチCPU/GPU環境とかだと、同一プラットフォームでCPUとGPUがそれぞれ1つ以上列挙される状況が発生するはずです。
OpenCLランタイム(OpenCL.dll)がどういうドライバー管理をしているのかいまいち把握しきれていないんですが(たぶんWindowsの場合レジストリ情報を見に行ってるっぽい?)、複数のCPU/GPUバイスや複数のSDKをインストールしている環境を考慮して、OpenCLターゲットとして使いたいデバイスがある場合は単純にプラットフォームやデバイスのインデックスを決めうちしないほうがいいでしょう。実際のアプリケーションではユーザーがOpenCL実行デバイスを選択できるようにしておいたほうがよいです。
OpenCL.dll自体はKhronosがライセンス管理してベンダーが再配布してるみたいですが、Windows標準コンポーネントではなく、SDKやドライバーをインストールするとシステムフォルダー(System32, SysWOW64)に同時にインストールされるものと思われます。
OpenCL、CUDA、それとOpenGL Compute Shaderは外部ランタイムやドライバーの追加インストールが必要になるので、単純にWindows上でGPGPUやりたいだけならば、ベンダー非依存のDirectComputeやC++ AMPを使うのが(エンドユーザー的には)楽だと思います。

なお、OpenCLの特徴のうち、CUDAやDirectComputeとの最大の違いは、ターゲット環境としてGPUだけでなくCPUも使えるってことなんですが、Intel OpenCL SDKに付いているサンプルでは、確かにデバイスとしてCPUが使われるようになっています。WindowsSDKに付いてくる「3D Fluid Simulation using OpenCL」という、D3D10のDXUTを使ったサンプルでは、OpenCL APIを使った場合と、C++でSSEを明示的に使った場合による比較ができるようになっているんですが、Core 2 Quad世代のXeonプロセッサを使ってみたところ、OpenCL APIを使ったほうが10%くらい高速になりました。

software.intel.com