データ並列 C++ のリダクション操作のパフォーマンスを解析する

インテル® oneAPIインテル® VTune™ プロファイラー

この記事は、『The Parallel Universe Magazine 45 号』に掲載されている「Reduction Operations in Data Parallel C++」の日本語参考訳です。


parallel_v45_02

前号の記事「データ並列 C++ のリダクション操作」では、総和演算子を使用して 1,000 万要素の配列を単一の値にレデュースするカーネル数を調査しました。この記事では、マルチブロック・インターリーブ・リダクションと呼ばれる別のリダクション手法を紹介します。第 9 世代と第 12 世代のインテル GPU でインテル® VTune™ プロファイラーを使用してすべてのリダクション操作を比較し、カーネル間のパフォーマンスの違いの理由を説明します。

マルチブロック・インターリーブ・リダクション

データ並列 C++ (DPC++) では、ショートベクトルをロード/ストアや算術演算子の操作が定義された基本データ型として定義しています。これらのショートベクトルを使用して別のレベルのブロックを追加し、サポートするアーキテクチャー向けに非常に長いベクトル演算をコンパイラーに生成させることができます。vec<int, 8> データ型は、8 つの整数からなるベクトルです。このデータ型を使用して、図 1 に示すリダクション操作を実装します。図に示すように、アクセスパターンはベクトルサイズが 2、サブグループ・サイズが 4 で、ワーク項目ごとに入力ベクトルの 4 要素を処理します。


図 1. 要素のベクトルを読み込み、その要素に対してベクトル・リダクション操作を行い、最終結果のベクトルをレデュース

以下のコードは、ベクトルサイズ 8、サブグループ・サイズ 16 で、ワーク項目ごとに入力ベクトルの 256 要素を処理する、上記のメモリー・アクセス・パターンのリダクション操作を実装します。

void multiBlockInterleavedReduction(sycl::queue &q, 
                    sycl::buffer<int> inbuf,
                    int &res) {
  const size_t data_size = inbuf.get_size()/sizeof(int);
  int work_group_size =
      q.get_device().get_info<sycl::info::device::max_work_group_size>();
  int elements_per_work_item = 256;
  int num_work_items = data_size / elements_per_work_item;
  int num_work_groups = num_work_items / work_group_size;
  sycl::buffer<int> sum_buf(&res, 1);

  q.submit([&](auto &h) {
      const sycl::accessor buf_acc(inbuf, h);
      sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::noinit);
      sycl::accessor<sycl::vec<int, 8>, 1, sycl::access::mode::read_write,
                     sycl::access::target::local>
          scratch(work_group_size, h);
      h.parallel_for(sycl::nd_range<1>{num_work_items, work_group_size},
                [=](sycl::nd_item<1> item) 
                [[intel::reqd_sub_group_size(16)]] {
         size_t glob_id = item.get_global_id(0);
         size_t group_id = item.get_group(0);
         size_t loc_id = item.get_local_id(0);
         sycl::ONEAPI::sub_group sg = item.get_sub_group();
         size_t sg_size = sg.get_local_range()[0];
         size_t sg_id = sg.get_group_id()[0];
         sycl::vec<int, 8> sum{0, 0, 0, 0, 0, 0, 0, 0};
         using global_ptr =
            sycl::multi_ptr<int,sycl::access::address_space::global_space>;
         int base = (group_id * work_group_size + sg_id * sg_size)
                            * elements_per_work_item;
         for (size_t i = 0; i < elements_per_work_item / 8; i++)
           sum += sg.load<8>(global_ptr(&buf_acc[base + i * 8 * sg_size]));
         scratch[loc_id] = sum;
         for (int i = work_group_size / 2; i > 0; i >>= 1) {
           item.barrier(sycl::access::fence_space::local_space);
           if (loc_id < i)
              scratch[loc_id] += scratch[loc_id + i];
         }
         if (loc_id == 0) {
            int sum=0;
            for (int i = 0; i < 8; i++)
               sum += scratch[0][i];
            auto v = sycl::ONEAPI::atomic_ref<int, 
                        sycl::ONEAPI::memory_order::relaxed,
                        sycl::ONEAPI::memory_scope::device,
                        sycl::access::address_space::global_space>(
                        sum_acc[0]);
            v.fetch_add(sum);
         }
      });
  });
}

このカーネルは、明示的にアドレスを計算する代わりに、ベクトルロード操作を使用してエンコードすることもできます。また、各ワーク項目がロードしたベクトルを最初にローカルでレデュースするように変更することもできます(この実装のアクセスパターンを図 2 に示します)。


図 2. 要素のベクトルを読み込み、ベクトルを単一の結果にレデュースしてからリダクションを行う

void multiBlockInterleavedReductionVector(sycl::queue &q, 
                    sycl::buffer<int> inbuf,
                    int &res) {
  const size_t data_size = inbuf.get_size()/sizeof(int);
  int work_group_size =
      q.get_device().get_info<sycl::info::device::max_work_group_size>();
  int elements_per_work_item = 256;
  int num_work_items = data_size / 4;
  int num_work_groups = num_work_items / work_group_size;
  sycl::buffer<int> sum_buf(&res, 1);

  q.submit([&](auto &h) {
      const sycl::accessor buf_acc(inbuf, h);
      sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::noinit);
      sycl::accessor<int, 1, sycl::access::mode::read_write,
                     sycl::access::target::local>
          scratch(1, h);
      h.parallel_for(sycl::nd_range<1>{num_work_items, work_group_size},
                [=](sycl::nd_item<1> item) 
                [[intel::reqd_sub_group_size(16)]] {
         size_t glob_id = item.get_global_id(0);
         size_t group_id = item.get_group(0);
         size_t loc_id = item.get_local_id(0);
         if (loc_id==0)
            scratch[0]=0;
         sycl::vec<int, 4> val;
         val.load(glob_id,buf_acc);
         int sum=val[0]+val[1]+val[2]+val[3];
         item.barrier(sycl::access::fence_space::local_space);
         auto vl = sycl::ONEAPI::atomic_ref<int,
                          sycl::ONEAPI::memory_order::relaxed,
                          sycl::ONEAPI::memory_scope::work_group,
                          sycl::access::address_space::local_space>(
                          scratch[0]);
         vl.fetch_add(sum);
         item.barrier(sycl::access::fence_space::local_space);
         if (loc_id==0) {
               auto v = sycl::ONEAPI::atomic_ref<int, 
                          sycl::ONEAPI::memory_order::relaxed,
                          sycl::ONEAPI::memory_scope::device,
                          sycl::access::address_space::global_space>(
                          sum_acc[0]);
               v.fetch_add(scratch[0]);  
       }
      });
  });
}

リダクション・カーネルのパフォーマンス解析

これらのカーネルのパフォーマンスを評価するため、2 つの異なるインテル GPU で実行します。

  1. インテル® HD グラフィックス 630(第 9 世代統合グラフィックス、略称 Gen9)。この GPU には 24 の実行ユニット(EU)が搭載されており、それぞれ 7 スレッドを利用できます。
  2. インテル® Iris® Xe グラフィックス(第 12 世代統合グラフィックス、略称 Gen12)。この GPU には 96 の実行ユニット(EU)が搭載されており、それぞれ 7 スレッドを利用できます。

インテル® VTune™ プロファイラーを使用してカーネルのパフォーマンスを解析します。また、カーネルの実行時間を長くして適切なプロファイル・データを収集できるように、大規模なリダクション(1,000 万要素の代わりに、5.12 億要素)を実行します。各カーネルのパフォーマンスを表 1 に示します。これは、各カーネルを 16 回実行して、平均パフォーマンスを記録したものです。データの収集には、インテル® oneAPI ベース・ツールキット(v2021.2.0)を使用しました。


製品とパフォーマンス情報

1実際の性能は利用法、構成、その他の要因によって異なります。詳細については、www.Intel.com/PerformanceIndex (英語) を参照してください。

タイトルとURLをコピーしました