SYCL 実装の性能を比較してみた

2023年2月20日

TL;DR: SYCL 2020 の実装を NVIDIA GPU で動かす手順の紹介と、畳み込み演算の性能比較

ソリューション第二事業部の坂部です。

以前(2017 年)当ブログにて、 SYCLを使ってOpenCLを単一ソースで書いてみる という記事が公開されました。 その記事は 2017 年時点の情報であり、2023 年の現時点ではいくぶんか状況が変わっていますので、当記事では新しい状況をお伝えします。

SYCL とは

単一の C++ ソースで、種々のアクセラレータ (GPU, FPGA など) を扱うための規格です。 SYCL を使うことで、ホスト向けとアクセラレータ向けのコードを別ファイルに分けることなく、 C++17 で記述できます。 また、CUDA や OpenCL と比較してレイヤの高いプログラミングパラダイムを持ち、ホスト・デバイス間のメモリ転送の記述を省略できる利点もあります。

規格の最新のバージョンは SYCL 2020 (Revision 6) です。

SYCL 2020 より過去のバージョン(SYCL 1.2 まで)は OpenCL に変換することを前提とした規格でしたが、 SYCL 2020 ではその他のバックエンドを実装できるようになりました。これにより、対応するアクセラレータの幅が広がっています。

SYCL の実装

Khronos の SYCL Overview サイト(2023/01/09 閲覧)によると、利用可能な実装は以下があります。

  • Intel oneAPI Data Parallel C++ (DPC++)
  • hipSYCL (注)
  • triSYCL
  • neoSYCL
  • Codeplay ComputeCpp

注: 2023 年 2 月に、 hipSYCL は Open SYCL に名称変更されました。 内容に変更はない ため、当記事では hipSYCL の名前のまま紹介します。

また、各実装がサポートするバックエンドやアクセラレータの一覧としては、 hipSYCL リポジトリのドキュメントにある図が分かりやすいです。 以下に引用します。


画像出典: https://github.com/illuhad/hipSYCL, Copyright (c) 2018 Aksel Alpay (BSD 2-Clause “Simplified” License)

本記事では、 NVIDIA GPU をターゲットにする場合に候補に挙がる

  • Intel oneAPI Data Parallel C++ (DPC++)
  • hipSYCL

の 2 通りの実装の使い方の紹介と、実装間の速度の比較を行いました。

NVIDIA GPU を使う場合において、 CUDA や OpenCL の環境構築に比べて SYCL の環境構築は複雑です。読者のみなさんが SYCL を試すときの手助けになれば幸いです。

実験環境

  • OS: Ubuntu 20.04.5 LTS
  • GPU: NVIDIA TITAN V (Volta)
    • CUDA 11.6

Intel oneAPI Data Parallel C++ (DPC++) の環境構築

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 に示されていますが、概要をまとめます。

  • 「Prerequisites」 (Git, CMake, Python, Ninja, GCC) をインストールする
  • CUDA 11.6 をインストールする
  • 作業ディレクトリを作り、リポジトリをクローンする
    $ 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 Corporation

hipSYCL の環境構築

hipSYCL (Open SYCL)を NVIDIA GPU とともに使用する場合の手順を紹介します。

LLVM/clang および hipSYCL コンパイラを入手し、ビルドする

手順は Building and installing hipSYCL および hipSYCL installation instructions for CUDA に示されていますが、概要をまとめます。

  • 「Software Dependencies」 (Python3, CMake, boost) をインストールする
  • CUDA 11.6 をインストールする
  • CUDA(NVPTX) サポートを有効にした LLVM/clang 14 をビルドする
    • LLVM/clang のビルド方法は Building LLVM with CMake を参照する
    • CUDA(NVPTX) サポート有効化のため、 configure 時に -D LLVM_TARGETS_TO_BUILD="X86;NVPTX" を付ける
  • hipSYCL リポジトリをクローンする
    $ git clone https://github.com/illuhad/hipSYCL
    $ cd hipSYCL
  • 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 SizeKernel SizeDPC++ Median (us)DPC++ Deviation (us)DPC++ Throughput (GFLOPS)hipSYCL Median (us)hipSYCL Deviation (us)hipSYCL Throughput (GFLOPS)
512×5123×3110.16400291.7160
512×5129×9430.46980580.93720
2048×20483×31101.06301105.4650
2048×20489×95503.412005401.41300

Throughput を見ると以下のことが分かります。

  • 512×512 画像では、どちらのカーネルサイズでも DPC++ が速い (1.3 から 2.5 倍)
  • 2048×2048 画像では、 DPC++ と hipSYCL は同等に速い

まとめ

今回の記事では以下を行いました。

  • Intel oneAPI DPC++ を使って NVIDIA GPU を扱う手順の紹介
  • hipSYCL を使って NVIDIA GPU を扱う手順の紹介
  • SYCL で畳み込みを実装し、同一 GPU で達成される速度を比較
    • 問題サイズが小さいときには DPC++ が優位だったが、問題サイズを大きくすると大きな差がない

参考文献

Tags

About Author

naoya.sakabe

Leave a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

Recent Comments

Social Media