このブログは、株式会社フィックスターズのエンジニアが、あらゆるテーマについて自由に書いているブログです。
TL;DR: SYCL 2020 の実装を NVIDIA GPU で動かす手順の紹介と、畳み込み演算の性能比較
ソリューション第二事業部の坂部です。
以前(2017 年)当ブログにて、 SYCLを使ってOpenCLを単一ソースで書いてみる という記事が公開されました。 その記事は 2017 年時点の情報であり、2023 年の現時点ではいくぶんか状況が変わっていますので、当記事では新しい状況をお伝えします。
単一の C++ ソースで、種々のアクセラレータ (GPU, FPGA など) を扱うための規格です。 SYCL を使うことで、ホスト向けとアクセラレータ向けのコードを別ファイルに分けることなく、 C++17 で記述できます。 また、CUDA や OpenCL と比較してレイヤの高いプログラミングパラダイムを持ち、ホスト・デバイス間のメモリ転送の記述を省略できる利点もあります。
規格の最新のバージョンは SYCL 2020 (Revision 6) です。
SYCL 2020 より過去のバージョン(SYCL 1.2 まで)は OpenCL に変換することを前提とした規格でしたが、 SYCL 2020 ではその他のバックエンドを実装できるようになりました。これにより、対応するアクセラレータの幅が広がっています。
Khronos の SYCL Overview サイト(2023/01/09 閲覧)によると、利用可能な実装は以下があります。
注: 2023 年 2 月に、 hipSYCL は Open SYCL に名称変更されました。 内容に変更はない ため、当記事では hipSYCL の名前のまま紹介します。
また、各実装がサポートするバックエンドやアクセラレータの一覧としては、 hipSYCL リポジトリのドキュメントにある図が分かりやすいです。 以下に引用します。
本記事では、 NVIDIA GPU をターゲットにする場合に候補に挙がる
の 2 通りの実装の使い方の紹介と、実装間の速度の比較を行いました。
NVIDIA GPU を使う場合において、 CUDA や OpenCL の環境構築に比べて SYCL の環境構築は複雑です。読者のみなさんが SYCL を試すときの手助けになれば幸いです。
Intel oneAPI Data Parallel C++ (DPC++) を NVIDIA GPU とともに使用する場合の手順を紹介します。
注: 2022年12月中旬、 oneAPI 2023.0 のリリースに際して oneAPI が NVIDIA GPU・AMD GPU のプラグインに対応したと発表されました。 当記事にこの内容は反映されておりません。
oneAPI DPC++ は apt などのパッケージシステムでインストールできますが、oneAPI 2023.0 以前は NVIDIA GPU の対応は GitHub で公開されている版のみでした。 GitHub 版 DPC++ を使用するには、まず DPC++ コンパイラをビルドする必要があります。
コンパイラをビルドする手順は Build DPC++ toolchain with support for NVIDIA CUDA に示されていますが、概要をまとめます。
$ export DPCPP_HOME=~/sycl_workspace  
$ mkdir $DPCPP_HOME 
$ cd $DPCPP_HOME 
$ git clone https://github.com/intel/llvm -b sycl --cuda を指定してビルドする
$ python $DPCPP_HOME/llvm/buildbot/configure.py --cuda   
$ python $DPCPP_HOME/llvm/buildbot/compile.py$DPCPP_HOME/llvm/build/bin/clang++ が生成されたことを確認する以下のサンプルコードを実行し、デバイス名とベンダー名を表示してみます。
使いたいデバイスの名前が表示されれば成功です。 DPC++ を使う環境ができました。
// show_names.cpp
#include <sycl/sycl.hpp>
#include <iostream>
int main() {
    // 使用可能デバイスの中からGPUを選ぶ
    auto device = sycl::device(sycl::gpu_selector_v);
    // デバイス名とベンダー名を取得し表示する
    std::cout << "Device Name: " << device.get_info<sycl::info::device::name>() << std::endl;
    std::cout << "Device Vendor: " << device.get_info<sycl::info::device::vendor>() << std::endl;
}# コンパイル
$ $DPCPP_HOME/llvm/build/bin/clang++ show_names.cpp -fsycl -fsycl-targets=nvptx64-nvidia-cuda -o show_names
# 実行(動的リンクのため、ライブラリ探索パスに llvm/build/lib を加える)
$ LD_LIBRARY_PATH="$DPCPP_HOME/llvm/build/lib:${LD_LIBRARY_PATH:-}" ./show_names
Device Name: NVIDIA TITAN V
Device Vendor: NVIDIA CorporationhipSYCL (Open SYCL)を NVIDIA GPU とともに使用する場合の手順を紹介します。
手順は Building and installing hipSYCL および hipSYCL installation instructions for CUDA に示されていますが、概要をまとめます。
-D LLVM_TARGETS_TO_BUILD="X86;NVPTX" を付ける$ git clone https://github.com/illuhad/hipSYCL
$ cd hipSYCL$ mkdir build
$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX=$HOME/local -DCUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda -DWITH_CUDA_BACKEND=ON -DBOOST_ROOT=/usr/include/boost ..
$ make install$HOME/local/bin/syclcc がインストールされていることを確認する以下のサンプルコードを実行し、デバイス名とベンダー名を表示してみます。
使いたいデバイスの名前が表示されれば成功です。 hipSYCL を使う環境ができました。
// show_names.cpp (DPC++ の環境構築のものと同じコード)
#include <sycl/sycl.hpp>
#include <iostream>
int main() {
    // 使用可能デバイスの中からGPUを選ぶ
    auto device = sycl::device(sycl::gpu_selector_v);
    // デバイス名とベンダー名を取得し表示する
    std::cout << "Device Name: " << device.get_info<sycl::info::device::name>() << std::endl;
    std::cout << "Device Vendor: " << device.get_info<sycl::info::device::vendor>() << std::endl;
}# LLVM/clang 14 をインストールしたディレクトリを syclcc 実行時に指定する
$ CLANG_ROOT=/opt/llvm-14/llvm-project/build/
# ビルドする
$ syclcc -O2 --hipsycl-targets='cuda:sm_70' --hipsycl-clang=$CLANG_ROOT/bin/clang++ --hipsycl-clang-include-path=$CLANG_ROOT/lib/clang/14.0.6 show_names.cpp -o show_names
$ ./show_names
Device Name: NVIDIA TITAN V
Device Vendor: NVIDIAこの節では、簡単な画像処理を実装し、2 通りの実装 (oneAPI DPC++, hipSYCL) の速度を比較します。
画像処理の内容は、畳み込み処理とします。
SYCL を使って愚直に実装すると、次のようなコードになります。 SYCL の使い方の詳しい部分については当記事で扱いません。
#include <cassert>
#include <iostream>
#include <sycl/sycl.hpp>
#include <opencv2/core/core.hpp>
void convolution2d(const cv::Mat& src, cv::Mat& dst, const cv::Mat& kernel)
{
    using inputType = std::uint8_t;
    assert(src.type() == CV_8UC1);
    assert(src.type() == dst.type());
    using kernelType = float;
    assert(kernel.type() == CV_32FC1);
    const int halfSize = kernel.cols / 2;
    // 利用可能デバイスのうち GPU を選択する
    auto device = sycl::device(sycl::gpu_selector_v);
    // プロファイル(時刻の記録)を有効化したキューを生成する
    auto q = sycl::queue{device, sycl::property::queue::enable_profiling{}};
    // 諸画像の範囲とバッファオブジェクトを定義する
    const auto srcRows = src.rows;
    const auto srcCols = src.cols;
    const auto srcRange = sycl::range<2>{
        static_cast<std::size_t>(srcRows), static_cast<std::size_t>(srcCols)
    };
    auto bufferSrc = sycl::buffer<inputType, 2>{
        src.ptr<inputType>(), srcRange
    };
    auto bufferDst = sycl::buffer<inputType, 2>{
        dst.ptr<inputType>(),
        sycl::range<2>{static_cast<std::size_t>(dst.rows), static_cast<std::size_t>(dst.cols)}
    };
    auto bufferKernel = sycl::buffer<kernelType, 2>{
        kernel.ptr<kernelType>(),
        sycl::range<2>{static_cast<std::size_t>(kernel.rows), static_cast<std::size_t>(kernel.cols)}
    };
    // キューに関数を登録する
    auto ev = q.submit([&](sycl::handler& cgh){
        auto accessorSrc = bufferSrc.get_access<sycl::access_mode::read>(cgh);
        auto accessorDst = bufferDst.get_access<sycl::access_mode::write>(cgh);
        auto accessorKernel = bufferKernel.get_access<sycl::access_mode::read>(cgh);
        cgh.parallel_for(
            srcRange,
            [accessorSrc, accessorDst, accessorKernel, halfSize, srcRows, srcCols](sycl::item<2> it) {
                float sum = 0.0f;
                const auto y = it.get_id(0);
                const auto x = it.get_id(1);
                // 画像の端は計算をスキップする
                if (   y < halfSize || y >= (srcRows - halfSize)
                    || x < halfSize || x >= (srcCols - halfSize)) {
                    return;
                }
                // 畳み込みを計算する
                for (int dy = -halfSize; dy <= halfSize; dy++) {
                    for (int dx = -halfSize; dx <= halfSize; dx++) {
                        sum += accessorSrc[y + dy][x + dx] * accessorKernel[dy + halfSize][dx + halfSize];
                    }
                }
                // 結果を出力画像に書き込む
                accessorDst[y][x] = sum;
            }
        );
    });
    // 計算の開始時刻と終了時刻を取得・表示する
    const auto startTime =
        ev.get_profiling_info<sycl::info::event_profiling::command_start>();
    const auto endTime =
        ev.get_profiling_info<sycl::info::event_profiling::command_end>();
    std::cout << startTime << ";" << endTime << std::endl;
}
int main() {
    constexpr int width = 512;
    constexpr int height = 512;
    constexpr int kernelSize = 3;
    cv::Mat src{height, width, CV_8UC1};
    cv::Mat dst{height, width, CV_8UC1};
    cv::Mat kernel{kernelSize, kernelSize, CV_32FC1};
    // 画像とカーネルの中身をここで設定する(省略)
    convolution2d(src, dst, kernel);
}畳み込みのように少々複雑な処理でも、 2 通りの実装で同じ結果を返す単一のコードを、容易に開発できています。
このコードでは SYCL のプロファイル機能を使って、計算の開始時刻と終了時刻を表示しています。
画像サイズとカーネルサイズをいくつか変えて実行した結果の表を以下に示します。 統計をとるための実行回数は 20 回とし、最初の 1 回に SYCL 処理系の初期化処理(DPC++ では 10 us 程度、 hipSYCL では 1 ms 程度)が含まれたため除外しています。 有効桁数を 2 桁で示しています。
| Image Size | Kernel Size | DPC++ Median (us) | DPC++ Deviation (us) | DPC++ Throughput (GFLOPS) | hipSYCL Median (us) | hipSYCL Deviation (us) | hipSYCL Throughput (GFLOPS) | 
|---|---|---|---|---|---|---|---|
| 512×512 | 3×3 | 11 | 0.16 | 400 | 29 | 1.7 | 160 | 
| 512×512 | 9×9 | 43 | 0.46 | 980 | 58 | 0.93 | 720 | 
| 2048×2048 | 3×3 | 110 | 1.0 | 630 | 110 | 5.4 | 650 | 
| 2048×2048 | 9×9 | 550 | 3.4 | 1200 | 540 | 1.4 | 1300 | 
Throughput を見ると以下のことが分かります。
今回の記事では以下を行いました。
keisuke.kimura in Livox Mid-360をROS1/ROS2で動かしてみた
Sorry for the delay in replying. I have done SLAM (FAST_LIO) with Livox MID360, but for various reasons I have not be...
Miya in ウエハースケールエンジン向けSimulated Annealingを複数タイルによる並列化で実装しました
作成されたプロファイラがとても良さそうです :) ぜひ詳細を書いていただきたいです!...
Deivaprakash in Livox Mid-360をROS1/ROS2で動かしてみた
Hey guys myself deiva from India currently i am working in this Livox MID360 and eager to knwo whether you have done the...
岩崎システム設計 岩崎 満 in Alveo U50で10G Ethernetを試してみる
仕事の都合で、検索を行い、御社サイトにたどりつきました。 内容は大変参考になりま...
Prabuddhi Wariyapperuma in Livox Mid-360をROS1/ROS2で動かしてみた
This issue was sorted....