oneAPI GPU 最適化ガイド

2024 年 7 月 18 日
リリース
2023 2024.2
年4月

oneAPI GPU 最適化ガイド

注意事項:
本ドキュメントはレイアウト調整および校閲を行っておりません。誤字脱字、製品名や用語の表記、レイアウト等の不具合が含まれる可能性があることを予めご了承ください。
この日本語マニュアルは、 インテル コーポレーションのウェブサイト で公開されている『 oneAPI GPU
Optimization Guide』 (バージョン 2024.2、更新日 2024/7/18) を iSUS 編集長が翻訳した参考訳です。
原文は更新される可能性があります。原文と翻訳文の内容が異なる場合は原文を優先してください。
インテル社の許可を得て iSUS (IA Software User Society) が翻訳版を作成した iSUS の著作物です。
原文は Intel Corporation の Copyright であり、日本語参考訳版にも適用されます。

♰

2

開発コード名oneAPI GPU 最適化ガイド

目次
1

はじめに ........................................................................................................................................................................ 5
生産性を高めるパフォーマンス ........................................................................................................................................... 5
パフォーマンス上の考察 ........................................................................................................................................................ 5
コードのプロファイルとチューニング ................................................................................................................................. 6
ソースコードの例 ..................................................................................................................................................................... 62
導入ガイド ..................................................................................................................................................................... 7
2-1
アムダールの法則.................................................................................................................................................................... 7
2-2
局所性の問題............................................................................................................................................................................ 7
2-3
ワークサイズ ............................................................................................................................................................................. 83並列処理........................................................................................................................................................................ 93-1
並列プログラミング言語や API を使用 ............................................................................................................................ 9
3-2
コンパイラー ............................................................................................................................................................................. 9
3-3
ライブラリー .............................................................................................................................................................................. 94
インテル® Xe GPU アーキテクチャー ...................................................................................................................... 10
4-1
Xe LP エグゼキューション・ユニット (EU) ........................................................................................................................ 10
4-2
Xe LP デュアル・サブスライス ............................................................................................................................................. 11
4-3
Xe LP スライス ........................................................................................................................................................................ 11
4-4
Xe コア ...................................................................................................................................................................................... 12
4-5
Xe スライス .............................................................................................................................................................................. 13
4-6
Xe スタック .............................................................................................................................................................................. 14
4-7
Xe HPC インテル® データセンター GPU マックス・シリーズ (2 スタック) ............................................................... 14
4-8
Xe HPG GPU ........................................................................................................................................................................... 15
4-9
Xe インテル® データセンター GPU フレックス・シリーズ ............................................................................................. 16
4-10
用語と構成のサマリー ..................................................................................................................................................... 16
5 GPU 上での汎用計算 .................................................................................................................................................... 18
5-1
GPU 実行モデル概要 ........................................................................................................................................................... 18
5-2
スレッドのマッピングと GPU 占有率 ............................................................................................................................... 20
5-3
カーネル ................................................................................................................................................................................... 32
5-4
GPU オフロードにライブラリーを使用 ......................................................................................................................... 122
5-5
ホスト/デバイスメモリー、バッファー、および USM ................................................................................................... 136
5-6
統合共有メモリーの割り当て .......................................................................................................................................... 136
5-7
USM とバッファーのパフォーマンスへの影響............................................................................................................. 138
5-8
ホストとデバイス間でデータ移動の往復を避ける ...................................................................................................... 142
5-9
データ転送の最適化 .......................................................................................................................................................... 146
5-10
ループ内でのバッファー宣言を避ける ...................................................................................................................... 154
5-11
バッファー・アクセサー・モード .................................................................................................................................... 156
5-12
ホスト/デバイスの調整 ................................................................................................................................................. 162
5-13
複数のヘテロジニアス・デバイスを使用.................................................................................................................... 166
5-14
コンパイル ....................................................................................................................................................................... 168
5-15
OpenMP* オフロードのチューニング・ガイド ......................................................................................................... 188
5-16
マルチ GPU、マルチスタック・アーキテクチャーとプログラミング ................................................................... 275
5-17
レベルゼロ ....................................................................................................................................................................... 347
5-18
パフォーマンスのプロファイルと解析 ....................................................................................................................... 348
5-19
GPU デバイスの構成 ..................................................................................................................................................... 353
6 GPU 上でのメディア・グラフィックス計算 ................................................................................................................. 355
6-1
メディア・パイプラインの最適化 ...................................................................................................................................... 355
6-2
インテル® グラフィックス・パフォーマンス・アナライザーによるパフォーマンス解析 ........................................ 367
1-1
1-2
1-3
1-4

3

oneAPI GPU 最適化ガイド
78

4

参考資料................................................................................................................................................................... 379
法務上の注意書き ................................................................................................................................................... 380

oneAPI GPU 最適化ガイド

1 はじめに高性能なソフトウェアを設計するには、ソフトウェアを作成する際に「通常とは異なる考え方」をする必要があります。
コードを実行するターゲット・ハードウェアと、そのハードウェアのパフォーマンス特性に十分な注意が必要です。皆さんの目的は、正しい結果を生成するようにコードを構造化することであると思いますが、それにはコードを実行する
ハードウェアの能力を最大限に引き出す方法を考慮しなければなりません。
oneAPI は、業界を超えたオープンな標準化をベースとした統一プログラミング・モデルであり、アクセラレーター・
アーキテクチャー全体で共通の開発手法を提供します。アクセラレーターのユニークな機能は、プラットフォームのメ
イン CPU に追加装着できるということです。そして、アクセラレーターを利用する主な利点は、ソフトウェアをホストとアクセラレーターに分割して、アクセラレーターに最適化された計算領域を特殊化することで、ソフトウェアの能力を改善できることです。アクセラレーターのアーキテクチャーは、特定クラスの計算向けに計算ハードウェアを特殊化することでメリットを得ています。これにより、アクセラレーターのアーキテクチャーに特化したソフトウェアに最適な結果を供給できます。
このドキュメントの主なターゲットは GPU であり、それぞれの節では、最適化されたソリューションを開発するための多くのトピックに焦点を当てています。インテル® oneAPI ツールキットは、コードの最適化に必要な言語と開発ツールを提供します。これには、コンパイラー、デバッガー、プロファイラー、アナライザー、およびライブラリーが含まれます。

1-1 生産性を高めるパフォーマンス
このドキュメントは GPU に注目しますが、CPU やそのほかのアクセラレーターで実行されるアプリケーションが必要になることも考えられます。アクセラレーターのアーキテクチャーは特殊化されているため、最高のパフォーマンス
を達成するにはコードも特殊化する必要があります。この特殊化には、アプリケーションからハードウェアへの最適なマッピングを見つけるためのコードの再構築とチューニングが含まれます。極端な場合には、適切な計算を実装する
ため、それぞれのアクセラレーターのアルゴリズムの再設計が必要になることもあります。oneAPI の素晴らしい点は、アクセラレーターで起動されたデバイス固有の機能を使用するため、それぞれのバリエーションを共通の言語で記述できることです。

1-2 パフォーマンス上の考察
GPU を利用する際の最初のフェーズは、アプリケーションのどの領域がオフロードの恩恵を得られるか特定することです。これは通常、計算に対するメモリーアクセスの比率が適切で、GPU にマッピングする適切なデータ依存のパターンを持つ計算集約型のコードであることがほとんどです。GPU にはローカルメモリーが備わっており、通常、大規模な並列処理を実行できます。そのため、どの領域を実行するか決定する際に、コードが持つ特性を理解することが最も重要となります。
インテル® oneAPI ベース・ツールキットに含まれるインテル® Advisor ツールは、コードを解析して並列実行に最適な場所を特定するように設計されています。インテル® Advisor のプロファイラーは、各種アクセラレーターにマッピングされた際にコードがどのように実行されるか予測するため、関数内のデータ移動、メモリー・アクセス・パターン、および計算量を測定します。潜在的に最も利点があるコード領域をアクセラレートする最初の対象とすべきです。
GPU は、多くの場合、複数レベルで並列処理を活用します。これには、ホストとデバイス間のオーバーラップ、計算コア間の並列処理、計算とメモリーアクセスのオーバーラップ、同時パイプライン、およびベクトル計算が含まれます。これらすべてのレベルで並列処理を行うには、GPU アーキテクチャーとライブラリー、および言語の機能を十分に理解する必要があります。

5

oneAPI GPU 最適化ガイド
すべての計算リソースを常にビジー状態に保つことも重要です。例えば、デバイスに 100 個の計算コアがあり、タスク
が 1 つしかない場合、デバイスの 99% はアイドル状態であると言えます。デバイスの飽和状態を保つには、ハードウェアが実行中のタスクの完了時に次のワークをスケジュールできるよう、利用可能な計算リソースよりもかなり多めの独立したタスクを生成する必要があります。
そして、ホストとデバイス間の同期を最小限に抑えます。ホストはデバイスでカーネルを起動し、その完了を待機します。カーネルの起動にはオーバーヘッドが生じるため、カーネルの起動回数を最小限にするように計算を構成します。
また、ホストとデバイス間のデータ転送を最小化することも考慮しなければなりません。データは通常ホストで準備され、計算の入力としてデバイスへコピーされます。そして、計算が完了した後、結果をホストへ転送する必要があります。
最高のパフォーマンスを得るには、デバイスの計算間で中間結果を保持することで転送を最小限に抑えます。また、計算とデータ移動をオーバーラップしてデータ転送の影響を減らし、計算コアがデータを待機する時間が生じないようにします。
データを高速メモリーに保持して適切なアクセスパターンを使用します。GPU アーキテクチャーには、アクセスコストが異なるさまざまなタイプのメモリーが搭載されています。レジスター、キャッシュ、またはスクラッチパッドは、ローカルメモリーよりもアクセスが容易ですが、容量は小さくなります。データがレジスター、キャッシュライン、またはメ
モリーページにロードされる場合、次のチャンクに移動する前にロードされたすべてのデータを利用するようなアクセ
スパターンを導入します。メモリーがバンク化されている場合、すべての計算コアが同じバンクに同時にアクセスするのを回避するようなストライドを適用します。

1-3 コードのプロファイルとチューニング
高いパフォーマンスを達成できるコードを設計したら、次のステップでは、ターゲット・アクセラレーターでコードがどのように実行されるか観測します。コードに時間計測コードを追加して、トレースを収集、またはインテル® VTune™
プロファイラーなどのツールを使用して実行中のプログラムを監視します。収集した情報からハードウェアのボトル
ネックとアイドル状態を特定して、ハードウェアのピーク・パフォーマンスと比較し、最適化の対象とすべき最も重要な
ホットスポットを見つけます。

1-4 ソースコードの例
本書では、実際のコード例を使用して最適化の手法を説明します。ここで使用するすべてのサンプルコードは、
oneAPI-samples GitHub* リポジトリー (英語) から入手できます。サンプルをダウンロードして、以下の手順の指示にしたがって、環境を設定してください。
それぞれのサンプルは、最適化の方法を示すため、可能な限りシンプルに、短く、そして分かりやすくしています。サン
プルコードは、ドキュメント全体で参照されます。
「百聞不如一見、百閒不如一為、故に、万聞不如一為」 (百聞は一見にしかず、の元になった孔子の言葉です) ということわざがあります。また、本書をお読みになっているときにサンプルコードに遭遇したら、読み進めるのをいったんやめて実機でサンプルを検証することをお勧めします。
ともあれ、oneAPI GPU 最適化ガイドへようこそ!!

6oneAPI GPU 最適化ガイド

2 導入ガイド
このドキュメントを最後まで読む時間がない読者は、少なくとも、アクセラレーター向けにソフトウェアを最適化する
次の 3 つの概念を理解してください。

2-1 アムダールの法則
これは当然のように思われますが、アクセラレーターを利用する最初のステップと言えます。アムダールの法則によれば、アプリケーションがアクセラレーターを使用する時間の比率 (Fp) によってアクセラレーションのメリットが制限され、最大スピードアップは、1/(1 − Fp) で制限されます。例えば、アクセラレーターを 50% 使用する場合、無限数のアクセラレーターを追加しても最大 2 倍のスピードアップしか得られません。
これは、プログラムのソースコードではなく実行に関連するものである、ということに注意してください。並列カーネル
は、ソースコード全体のごく一部である可能性がありますが、実行時間が集中している場所であれば、それで問題ないでしょう。

2-2 局所性の問題
多くの場合、アクセラレーターには互いに独立したアドレス空間を持つ特殊なメモリーが備えられています。そのため、アプリケーションは、適切なタイミングで適切なメモリーにデータを割り当てるか、移動する必要があります。
アクセラレーターのメモリーは階層的に配置されます。レジスターはキャッシュよりもアクセス効率が良く、キャッ
シュはメインメモリーよりも効率良いアクセスが可能です。データを実行ポイントに近づけることで効率は向上します。
コードを再構成してデータを実行ポイントに近づける方法はたくさんあります。これらについては以下で説明します。
ここでは次の 3 つに注目します。
1.

アクセラレーターにデータを割り当ててデータをコピーする場合、可能な限りデータの常駐を維持します。アプ
リケーションには多数のオフロード領域が含まれる場合がありますが、それらの領域間で共通のデータがある
場合、最初のコピーのコストを償却するため、残りのカーネル呼び出しでデータを再利用するのは理にかなって
います。

2.

カーネルの実行中は連続したメモリーブロックにアクセスします。ハードウェアは連続するブロックをメモリー階層にフェッチするため、ブロック単位で転送のコストが生じます。ブロックの最初の要素を参照すると、残りの要素は低いコストでアクセルできるためできるだけそれらを使用します。

3.

データの再利用率が高いブロックにコードを再構成します。2 次元行列では、次の要素ブロックにアクセスする前に、要素を含む 1 ブロックを処理するようにワークを調整できます。例えば、ステンシル操作では、前の行、
現在の行、そして次の行にアクセスできます。ブロック内の要素をすべてアクセスすると、データが再利用されメモリーにデータを要求するコストを排除できます。

7

oneAPI GPU 最適化ガイド

2-3 ワークサイズ
データ並列アクセラレーターは、スループット・エンジンとして設計されており、エグゼキューション・ユニットを繰り返し複製することに特化しています。これにより、より多くの要素を同時に処理することができますが、データ並列アルゴ
リズムで高いパフォーマンスを得るには厄介であり、並列プロセッサーを完全に利用するのは困難な場合があります。
例えば、512 個のエグゼキューション・ユニットがあり、それぞれのエグゼキューション・ユニットは 8 つのスレッドを実行し、スレッドには 16 要素のベクトルがあると仮定します。これに一致するようにスケジュールを行うには、最低でも 512 x 8 x 16 = 65536 の並列アクティビティーが必要となります。さらに、それぞれの並列アクティビティーが小さい場合、このワークをアクセラレーターに送信するコストを償却するには、また別の大きな要素が必要です。単一の大規模なアクセラレーターを完全に利用するには、計算を数百万の並列アクティビティーに分割することが求められることもあります。

8

oneAPI GPU 最適化ガイド

3 並列処理アクセラレーターはコードを並列に実行できる多くの独立した処理要素を備えているため、並列処理はアクセラレー
ターを効率良く利用するには不可欠です。並列コードを開発するには 3 つの方法があります。

3-1 並列プログラミング言語や API を使用
並列処理を表現するために利用できる並列プログラミング言語と API は各種存在します。oneAPI は、SYCL* フレー
ムワークによる並列プログラム開発をサポートします。また、インテル® oneAPI では、これらのプログラムをさまざまなアクセラレーターで実行可能なバイナリーに変換する、コード生成ツールも多数用意されています。通常の作業工程では、シリアルプログラムを実行し、時間を要するコード領域 (ホットスポットと呼ばれる) を識別して、それらを並列カーネルに変換し、アクセラレーターに送信して実行します。

3-2 コンパイラー
OpenMP* のようなディレクティブ・ベースの手法は、並列プログラムを開発するもう 1 つの方法です。ディレクティブ・
ベースのアプローチでは、プログラマーはコード自身を明示的に変更することなく、コンパイラーに並列処理に関するヒントを提供します。このアプローチは、スクラッチから並列プログラムを開発するよりも簡単です。

3-3 ライブラリー
oneAPI には、oneTBB、oneMKL、インテル® ディープ・ニューラル・ネットワーク・ライブラリー (oneDNN)、および
インテル® ビデオ・プロセシング・ライブラリー (インテル® VPL) など多くのライブラリーが含まれており、さまざまなア
クセラレーター・アーキテクチャーで実行される高度にさて気化された汎用的な計算操作のバージョンが用意されています。アプリケーションのニーズに応じて、開発者はこれらのライブラリーの関数を直接呼び出すことで、ベースとなるアーキテクチャー向けの効率良い実装を選択できます。必要とする関数がライブラリー含まれている場合、これは並列プログラムを開発する最も簡単なアプローチと言えます。例えば、oneDNN はマシンラーニング・アプリケー
ション向けに最適化されたプリミティブを提供します。これらのライブラリーは、正当性とパフォーマンスの両面で徹底した検証が行われており、プログラムの信頼性を高めます。

9

oneAPI GPU 最適化ガイド

4 インテル® Xe GPU アーキテクチャー
インテル® Xe GPU ファミリーは、統合/低電力 (Xe LP) から、愛好家/高性能ゲーム (Xe HPG)、データセンター/AI (XeHP)、およびハイパフォーマンス・コンピューティング (Xe HPC) 向けの一連のアーキテクチャーで構成されています。
インテル® Iris® Xe ファミリー

この節では、インテル® Xe GPU ファミリーのマイクロアーキテクチャーと設定パラメーターについて説明します。

4-1 Xe LP エグゼキューション・ユニット (EU)
エグゼキューション・ユニット (EU) は、インテル® Xe LP GPU アーキテクチャーにおける最も小さなスレッドレベルの構成要素です。各 EU は 7 つのスレッドによる同時マルチスレッド (SMT) を実装します。主な計算ユニットは、SIMD8
FP/INT 操作をサポートする 8 要素の SIMD （Single Instruction Multiple Data) 演算ロジックユニット (ALU) と、
SIMD2 拡張数学操作をサポートする 2 要素の SIMD ALU から構成されています。それぞれのハードウェア・スレッド
は、３２B 幅の 128 個の汎用レジスター (GRF) を持ちます。
Xe LP-EUXe LP EU は、AI アプリケーション向けに FP16、INT16 および INT8 データタイプをサポートします。次の表では、Xe LP における各種データタイプの EU スループットを比較しています。

10

oneAPI GPU 最適化ガイド
インテル® Xe LP GPU 計算スループット率 (ops/clock/EU)
FP32
8

FP16
16

INT32
8

INT16
16

INT 8
32 (DP4A)

4-2 Xe LP デュアル・サブスライス
それぞれの Xe LP デュアル・サブスライス (DSS) は、16 個の EU アレイ、命令キャッシュ、ローカル・スレッド・ディス
パッチャー、共有ローカルメモリー (SLM)、および 128B/サイクルのデータポートで構成されます。ハードウェアがSIMD16 を実行するため 2 つの EU をペアにするので、デュアルサブスライスと呼ばれます。
SLM は、サブスライス内の EU からアクセス可能な 128KB の低レイテンシーおよび高帯域幅メモリー (HBM) です。
SLM の重要な用途の 1 つは、サブスライス内で同時に実行されるワーク項目間でアトミックデータとシグナルを共有することです。そのため、カーネルのワークグループに同期処理が含まれる場合、ワークグループ内のすべてのワー
ク項目は単一のサブスライスに割り当てられ、同じ 128KB SLM に共有アクセスできるようにする必要があります。サブスライスの占有率と利用率を最大化するため、ワークグループのサイズを注意深く選択しなければなりません。一方、カーネルが SLM にアクセスしない場合、ワーク項目は複数のサブスライスにまたがってディスパッチされる可能性があります。
次の表は、サブスライスの計算能力をまとめたものです。
インテル® Xe LP サブスライスの計算能力
EU 数
16

スレッド数
7 x 16 = 112

操作
112 x 8 = 896

4-3 Xe LP スライス
各 Xe LP スライスは 6 つのサブスライス (デュアル) で構成され、合計 96 個の EU、最大 16MB の L2 キャッシュ、L2への 128B/サイクル帯域幅、および 128B/サイクルのメモリー帯域幅を持ちます。

11

oneAPI GPU 最適化ガイド
Xe LP スライス

4-4 Xe コア
Xe LP や以前のインテル® GPU では、計算ユニットをエグゼキューション・ユニット (EU) と呼んでいましたが、
Xe HPG と Xe HPC では Xe コアと呼称します。これは、Xe LP デュアル・サブスライスと同様です。
Xe コアは、ベクトルエンジンおよび行列エンジンと呼ばれるベクトル ALU と行列 ALU を搭載しています。
Xe コアは、8 つのベクトルエンジン、8 つの行列エンジン、および 512KB 大容量キャッシュ/SLM を搭載しています。
これは、インテル® データセンター GPU マックス・シリーズの中核となります。各ベクトルエンジンは 512 ビット幅で、融合型 FMA による 16 個の FP32 SIMD 操作をサポートしています。8 つのベクトルエンジンを搭載する Xe コアは、
512 FP16、256 FP32 および 128 FP64 操作/サイクルを実現します。
各行列エンジンは 4096 ビット幅です。8 つの行列エンジンを搭載する Xe コアは、8192 int8、4096 FP16/BF16 お
よび 2048 FP32 操作/サイクルを実現します。また、Xe コアはメモリーシステムに対して 1024B/サイクルのロード/ストア帯域幅を提供します。

12

oneAPI GPU 最適化ガイド
Xe コア

4-5 Xe スライス
Xe スライスには、合計 8MB L1 キャッシュを持つ 16 個の Xe コア、16 個のレイトレーシング・ユニット、1 個のハード
ウェア・コンテキストが含まれます。
Xe スライス13

oneAPI GPU 最適化ガイド

4-6 Xe スタック
Xe スタックは最大 4 つの Xe スライスを含みます。64 個の Xe コア、64 個のレイトレーシング・ユニット、4 個のハード
ウェア・コンテキスト、4 個の HBM2e コントローラー、1 つのメディアエンジン、および 8 つの Xe リンク・ハイスピー
ド・コヒーレント・ファブリックを搭載します。また、共有 L2 キャッシュも含みます。
Xe スタック

4-7 Xe HPC インテル® データセンター GPU マックス・シリーズ (2 ス
タック)
Xe HPC インテル® データセンター GPU マックス・シリーズ (2 スタック) は 2 つのスタックで構成されます。8 個のス
ライス、128 個の Xe コア、128 個のレイトレーシング・ユニット、8 個のハードウェア・コンテキスト、8 個の HBM2e お
よび 16 個の Xe リンクが含まれます。

14

oneAPI GPU 最適化ガイド
Xe HPC 2 スタック

4-8 Xe HPG GPU
Xe HPG は、Xe アーキテクチャーのエンスージアストまたはハイパフォーマンス・ゲーミング向けのバリエーションです。これは、グラフィックス・パフォーマンスに特化したマイクロアーキテクチャーであり、ハードウェア・アクセラレー
ションによるレイトレーシングをサポートします。
各 Xe コア には、16 個のベクトルエンジン、16 個の行列エンジンが搭載されています。これは、インテル® Arc™ GPUの中核となります。各ベクトルエンジンは 256 ビット幅で、融合型 FMA による 8 個の FP32 SIMD 操作をサポートしています。16 つのベクトルエンジンを搭載する Xe コア は、サイクルあたり 256 FP32 操作を実現します。各行列エンジンは 1024 ビット幅です。16 つの行列エンジンを搭載する Xe コアは、サイクルあたり 4096 int8 操作、およびサ
イクルあたり 2048 FP16/BF16 操作を実現します。また、Xe コアはメモリーシステムに対して 512B/サイクルのロード/ストア帯域幅を提供します。

15

oneAPI GPU 最適化ガイド
Xe HPG GPU は、8 つの Xe HPG スライスで構成され、最大 4 つの Xe HPG コアがあり、合計 4096 FP32 ALU ユニット/シェーダーコアが含まれます。

4-9 Xe インテル® データセンター GPU フレックス・シリーズ
インテル® データセンター GPU フレックス・シリーズには、2 つの構成があります。150W オプション構成では、PCIe*
4.0 カードに 32 個の Xe コアがあります。75W オプションには、16 Xe コア (GPU あたり 8 Xe コア) の 2 つの GPUがあります。どちらの構成にも、4 つの Xe メディアエンジン、データセンター向けでは業界初の AV1 ハードウェア・エンコーダーおよびアクセラレーター、GDDR6 メモリー、レイトレーシング・ユニット、ビルトインのインテル® XMX AI
アクセラレーションが付属しています。
インテル® データセンター GPU フレックス・シリーズは、Xe HPG GPU の派生品であり、インテル® データセンター
GPU フレックス 170 は 4096 FP32 ALU ユニット/シェーダーコアを備えた合計 32 Xe コアの 8 つの Xe HPG スライスで構成されます。
データセンターにおけるクラウドゲーム、メディア・ストリーミング、およびビデオ分析アプリケーション向けのインテル®データセンター GPU フレックス・シリーズは、ハードウェア・アクセラレーションによる AV1 エンコーダーを備えているため、品質を損ねることなくビットレートを 30% 向上します。カードごとに 8 つの同時 4K ストリームまたは 30 以上の 1080p ストリームをサポートします。AI モデルは、インテル® データセンター GPU フレックス・シリー
ズの Xe コアを利用したデコードストリームに適用できます。
メディア・ストリーミングおよび配信ソフトウェア・スタックは、AV1 を含むすべての主要なコーデックのデコードとエンコードのアクセラレーションにインテル® ビデオ・プロセシング・ライブラリー (インテル® VPL) を採用しています。
メディア配信事業者は、2 つの主要なメディア・フレームワークである FFMPEG* または GStreamer を選択でき、どちらもインテル® CPU とインテル® GPU 上でインテル® VPL によるアクセラレーションが可能です。
メディアストリームのデコードとエンコードをアクセラレートするインテル® VPL と並行して、oneAPI ディープ・
ニューラル・ネットワーク・ライブラリー (oneDNN) は、TensorFlow* や PyTorch* フレームワークの推論モードをア
クセラレートするのに有効な AI 最適化カーネル、または OpenVINO™ モデル・オプティマイザーや推論エンジンを提供し、推論をさらに加速してユーザーのワークロードの展開をスピードアップします。

4-10 用語と構成のサマリー
次に示すアーキテクチャー用語の変更の表は、従来の GPU 用語 (第 9 世代から第 12 世代インテル® Core™ アーキ
テクチャーで使用された) を、インテル® Xe GPU アーキテクチャー (12.7 世代以降) と比較して新しい用語に対応させたものです。
アーキテクチャー用語の変更
古い用語
エグゼキューション・ユニット(EU)
シストリック/「EU の DPAS 部分」
サブスライス (SS) または
デュアル・サブスライス (DSS)
スライス
タイル

16

新しい用語
Xe ベクトルエンジン

一般的な用語
ベクトルエンジン

新しい略語
XVE

Xe 行列拡張

行列エンジン

XMX

Xe コア

NA

XC

レンダースライス/計算スライス
スタック

スライス
スタック

SLC
STK

oneAPI GPU 最適化ガイド
次の X の表は、Xe ファミリー GPU 全体のハードウェア特性を示しています。
Xe の構成アーキテクチャー

Xe LP (TGL✝)Xe HPG (インテル®
データセンター GPU
フレックス 170)

Xe HPC (インテル®
データセンター GPU
MAX 1550)

1
6
16
96
7
672
1536
NA
NA
r:128、w:128

Xe HPG(インテル® Arc™
A770 グラフィック
ス)
8
32
16
512
8
4096
8192
NA
65536
r:512、w:512

スライス数
XC (DSS/DSS) 数
XVE (EU)/XC
XVE 数
スレッド/XVE
スレッド数
FLOPs/clk - 単精度、MAD
FLOPs/clk - 倍制度、MAD
FLOPs/clk - FP16 DP4AS
GTI 帯域幅 バイト/
非スライス – clk
LL キャッシュサイズ
SLM サイズ
FMAD、SP (ops/XVE/clk)
SQRT、SP (ops/XVE/clk)

8
32
16
512
8
4096
8192
NA
65536
r:512、w:512

4x2
64 x 28512 x 284096 x 2
16384 x 2
16384 x 2
262144 x 2
r:1024、w:1024

3.84MB
6 x 128KB
8
2

16MB
32 x 128KB
8
2

16MB
32 x 128KB
8
2

最大 408MB
64 x 128KB
16
4

17

oneAPI GPU 最適化ガイド

5 GPU 上での汎用計算
従来、GPU は画像や動画などコンピューター・グラフィックスの作成で使用されていました。近年の GPU は、超並列処理向けのエグゼキューション・ユニットも多数備わっているため、これまで CPU で実行されていたタスクの計算にも利用されています。これは一般に、GPU または GPGPU による汎用コンピューティングと呼ばれます。
ハイパフォーマンス・コンピューティングやマシンラーニングのアプリケーションは、GPGPU から大きな恩恵を受け
ています。














GPU 実行モデル概要
スレッドのマッピングと GPU 占有率
カーネルGPU オフロードにライブラリーを使用
ホスト/デバイスメモリー、バッファー、および USM
ホスト/デバイスの調整
複数のヘテロジニアス・デバイスを使用
コンパイル
OpenMP* オフロードのチューニング・ガイド
マルチ GPU、マルチスタック・アーキテクチャーとプログラミング
レベルゼロ
パフォーマンスのプロファイルと解析
GPU デバイスの構成

5-1 GPU 実行モデル概要
汎用 GPU (GPGPU) の計算モデルは、1 つ以上の計算デバイスに接続されたホストで構成されます。各計算デバイス
は、エグゼキューション・ユニット (EU) またはベクトルエンジン (XVE または VE) と呼ばれる多数の GPU 計算エンジン (CE) で構成されています。また、計算デバイスは、次の図に示すように、キャッシュ、共有ローカルメモリー (SLM)、
高帯域幅メモリー (HBM) などを含む場合があります。アプリケーションは、ホスト上のソフトウェア (ホスト・フレーム
グラフごと) と、ホストから送出されるカーネルの組み合わせとして構築され、事前に定義された分岐ポイントで VE
上で実行されます。

18

oneAPI GPU 最適化ガイド
汎用計算モデルGPGPU 計算アーキテクチャーは、ホストプログラムと、ホストで設定されたコンテキスト内で実行されるカーネル
セットの 2 つの異なる実行単位を含んでいます。ホストは、コマンドキューを介してこれらのカーネルと対話します。
各デバイスは独自のコマンドキューを保持することができます。コマンドキューにコマンドが投入されると、コマンドの依存関係がチェックされ、計算ユニットクラスター内の VE で実行されます。コマンドの実行が完了すると、カーネルは「スレッドの終了」メッセージによってライフサイクルの終了を通知します。
GP 実行モデルでは、カーネルをどのようにスケジュールして実行するかを決定します。kernel-enqueue コマンドが実行のためカーネルを送出するときに、コマンドはインデックス空間または N 次元のレンジを定義します。kernelinstance は、カーネルそしてカーネルに関連する引数値、およびインデックス空間を定義するパラメーターで構成されます。計算デバイスが kernel-instance を実行する際に、カーネル関数は定義されたインデックス空間、または N
次元のレンジ内の各ポイントに対し実行されます。
実行中のカーネル関数は work-item (ワーク項目) と呼ばれ、その集合を work-group (ワークグループ) と呼びます。
計算デバイスは、work-group によって work-item を管理します。それぞれの work-item は、グローバル ID、または
work-group ID と work-group 内のローカル ID の組み合わせで識別されます。
work-group の概念は、基本的にグループ内の複数のユニット項目で同じカーネルを実行するものであり、データ並列計算の本質であると考えられます。VE は、work-item を SIMD ベクトル形式で構成することで SIMD ベクトル上で同じカーネルを実行できるため、該当するアプリケーションの計算を高速化できます。
デバイスは、各 work-group を任意の順番で計算できます。また、1 つの work-group 内の work-item は同時に実行されますが、実行の順番は保証されません。バリアのような高レベルの work-group の同期機能は、work-group内の各 work-item に適用され、必要とされる同期ポイントを容易にします。このような work-group 機能は、workgroup 内のすべての work-item が正確に同じ work-group 機能に到達するように定義されなければなりません。

19

oneAPI GPU 最適化ガイド
同期はコマンドレベルでも発生する可能性があり、ホストのコマンドキューにあるコマンド間で同期が行われることがあります。このモードでは、あるコマンドが別のコマンド、または複数のコマンドの実行ポイントに依存関係を持つ
ことがあります。
プログラム内部のメモリー順序の制約に関する同期として、アトミックやフェンスがあります。これらの同期タイプは、
特定の work-item のメモリー操作をほかの work-item からどのように見えるかを制御します。これにより、データ並
列計算モデルにおけるマイクロレベルの同期ポイントが提供されます。
インテルの GPU デバイスは、多数のベクトルエンジン (VE) を搭載し、それぞれの VE はマルチスレッドの SIMD プロ
セッサーです。コンパイラーは、ハードウェア・スレッド内で同時に実行される複数の work-item をマップする SIMD
コードを生成します。カーネルの SIMD 幅は、ヒューリスティックによりコンパイラーが選択します。一般的な SIMD 幅
は、SIMD-8、SIMD-16、SIMD-32 です。
特定の SIMD 幅に対して、スレッド内のすべてのカーネル・インスタンスが同じ命令を実行する場合、SIMD レーンを最大限に活用できます。カーネル・インスタンスの 1 つ以上が分岐先に分岐した場合、スレッドは分岐の 2 つの経路を実行し、マスクによって結果をマージします。VE の分岐ユニットは、このような分岐や分岐の入れ子を記録します。

5-2 スレッドのマッピングと GPU 占有率
SYCL* 実行モデルは、インテル® アーキテクチャーを採用する GPU の抽象化ビューを提供します。SYCL* スレッド階
層は、work-item の 1、2、または 3 次元のグリッドで構成されます。これらの work-item は、work-group と呼ばれる同一サイズのスレッドのグループにグループ化されます。そして、work-group のスレッドは、sub-group と呼ばれる同じサイズのベクトルのグループに分割されます。
work-item (ワーク項目)
work-item は、カーネルの並列実行のコレクションの 1 つを表します。
sub-group (サブグループ)
sub-group は、レングスが 8、16、32 の SIMD ベクトル、またはインテル® UHD グラフィックスを搭載する
CPU のネイティブベクトル長の倍数として同時に処理される、短い範囲で連続した work-item を表します。
work-group (ワークグループ)
work-group は、スレッド階層内の 1、2、または 3 次元のスレッドのセットです。SYCL* における work-item
間の同期は、同一 work-group 内で work-item のバリアがある場合にのみ可能です。

20

oneAPI GPU 最適化ガイド

5-2-1

nd_rangend_range は、スレッド階層を 1、2、または 3 次元の work-group のグリッドに分割します。これは、各 work-groupのローカルレンジであるグローバルレンジとして表現されます。
スレッド階層上の図は、nd_range、work-group、sub-group、および work-item の関係を示しています。

5-2-2

スレッドの同期

SYCL* は、カーネル関数内で呼び出すことができる 2 つの同期メカニズムを提供します。どちらも、同じ work-group内の work-item に対し定義されます。SYCL* は、nd_range 全体のすべての work-item に対するカーネル内でのグ
ローバル同期メカニズムを提供していません。
•

mem_fence は、work-group 内のすべての work-item 間のグローバルおよびローカル・メモリー・アクセスに対しメモリーフェンスを挿入します。

•

barrier はメモリーフェンスを挿入して、work-item がその場所に達するまで、work-group 内のすべてのwork-item の実行をブロックします。

5-2-3

最大の占有率を得るように work-group を Xe コアへマップ

この章の残りでは、GPU リソースの占有率を最大化するために必要な、適切な work-group サイズの選択方法について説明します。ここでは、実行ターゲットとして、Xe LP GPU を搭載するインテル® プロセッサー (Tiger Lake✝、略称
TGL✝) を使用します。この例では、デュアル・サブスライスには Xe コア (XC)、エグゼキューション・ユニットには Xe ベ
クトルエンジン (XVE) という新しい用語を使用しています。
以下の表に、Xe LP グラフィックス (TGL✝) GPU のアーキテクチャー・パラメーターをまとめています。
Xe LP (TGL✝) GPU

各 Xe コア
合計

VE 数

スレッド数

操作

16
16 x 6 = 96

7 x 16 = 112
112 x 6 = 672

112 x 8 = 896
896 x 6 = 5376

最大ワークグループ・
サイズ
512
512

21

oneAPI GPU 最適化ガイド
最大 work-group サイズは、ハードウェアと GPU ドライバーによって制限されます。サポートされる work-group の最大サイズは、device::get_info<cl::sycl::info::device::max_work_group_size>() で照会できます。
簡単なカーネルの例を見ていきましょう。
1.
2.
3.
4.
5.
6.
7.

auto command_group =[&](auto &cgh) {cgh.parallel_for(sycl::range<3>(64, 64, 64), // グローバルレンジ[=](item<3> it) {// (カーネルコード)})}

このカーネルには、64 x 64 x 64 の 3D レンジとして構造化された 262,144 個の work-item が含まれます。workgroup と sub-group サイズの選択は、コンパイラーに任されます。GPU スライスで使用可能な 5,376 の並列操作を完全に利用するには、コンパイラーが適切な work-group サイズを選択しなければなりません。
最も重要な 2 つの GPU リソースは以下です。
•

スレッド・コンテキスト: カーネルには、GPU のスレッド・コンテキストを活用するのに十分な数のスレッドが必要です。

•

SIMD ユニットと SIMD レジスター: カーネルは、work-item をベクトル化し、SIMD レジスターを利用するように構成する必要があります。

SYCL* カーネルでは、プログラマーはカーネルを適切な work-group サイズ、sub-group サイズに構造化し、効率良いベクトル実行のため work-item を構成することで、ワークの分散を調整できます。効率良いベクトルカーネルの記述方法については、別の節で説明します。この章では work-group と sub-group のサイズの選択にのみ焦点を当てます。
スレッド・コンテキストは SIMD ベクトルよりも容易に利用できます。したがって、work-group 内のスレッド数を選択することから始めます。各 Xe コアには 112 のスレッド・コンテキストがありますが、カーネルが 8 でベクトル化されている場合 (112 x 8 = 896 > 512)、すべてのスレッドを利用することができません。このことから、work-group 内のス
レッドの最大数は 64 (512/8) であることが分かります。
SYCL* は work-group 内のスレッド数を直接設定するメカニズムを持っていませんが、work-group サイズと subgroup サイズからスレッド数を求めることができます。
work-group サイズ = スレッド数 x sub-group サイズ
カーネルに十分な数のレジスターがある限り、sub-group のサイズを増やすことができます。各 VE には 8 つの 128
ビット SIMD レジスターがあることに留意してください。sub-group サイズを増やす効果はループのアンロールに似ています。各 VE は、サイクルごとに 8 つの 32 ビット操作を実行しますが、work-group の相互作用ごとのワーク量は 2 倍もしくは 4 倍になります。SYCL* では、プログラマーは intel::reqd_sub_group_size({8|16|32})を使用して sub-group のサイズを明示的に指定し、コンパイラーの選択をオーバーライドできます。
次の表は、スレッド数の選択基準、インテル® Iris® Xe LP GPU のすべての GPU リソースを占有できる sub-group の
サイズをまとめたものです。

22

oneAPI GPU 最適化ガイド
完全な占有率を達成する構成
最大スレッド数64

最小サブグループ・サイズ
8

最大サブグループ・
サイズ
32

最大ワークグループ・
サイズ
512

条件
スレッド数 × サブグループ・
サイズ <= 512

一般に、大きな work-group サイズを選択すると、work-group をディスパッチする回数を減らすことができる利点があります。sub-group のサイズを大きくすると、work-group で必要なスレッド数を減らすことができますが、subgroup ごとの待機時間が長くなり、レジスターの負荷が増加します。
work-group 内での work-item の同期の影響
work-item の同期を必要とするカーネルの例を示します。
1.
2.
3.
4.
5.
6.
7.

auto command_group =[&](auto &cgh) {cgh.parallel_for(nd_range(sycl::range(64, 64, 128), // グローバルレンジ
sycl::range(1, R, 128)),[=](sycl::nd_item<3> item) {// (カーネルコード)

8.
9.
10.
11.
})
12. }

// ローカルレンジ// 内部初期化
item.barrier(access::fence_space::global_space);// (カーネルコード)

このカーネルは、work-group バリアの同期が必要なことを除けば前の例と似ています。work-item の同期は、同じ
work-group 内の work-item 間でのみ利用できます。プログラマーは、nd_range と nd_item を使用して workgroup のローカルレンジを選択する必要があります。work-group のすべての work-item は同じ Xe コアに割り当てられるため、Xe コアの占有率とカーネルのパフォーマンスに影響します。
このカーネルの work-group のローカルレンジは、range(1, R, 128) として与えられます。sub-group サイズが8 であると仮定すると、変数 R の値が VE の占有率にどのように影響するか確認してみます。R=1 の場合、ローカルグ
ループのレンジは (1, 1, 128) で、work-group のサイズは 128 です。使用可能なスレッド・コンテキストは 112 ありますが、work-group に割り当てられた Xe コアには 16 スレッドしか含まれていません (占有率が非常に低い)。ただし、システムは 7 つの work-group を同じ Xe コアにディスパッチすることで、ディスパッチ数を増やすことと引き換えにスレッドの占有率を高めることができます。
R>4 の場合、work-group のサイズはシステムでサポートされる最大 work-group サイズ 512 を超えるため、カーネルの起動に失敗します。R=4 の場合、Xe コアは 57% しか占有されておらず (4/7)、3 つの未使用スレッド・コンテキス
トは別の work-group に対応するには十分でなく、利用可能な VE の 43% が浪費されています。ドライバーは、未使
用の Xe コアに部分的な work-group をディスパッチできる可能性があることに注意してください。ただし、カーネルにはバリアがあるため、部分的にディスパッチされた work-item は残りの work-group がディスパッチされるまでバリアを通過できません。多くの場合、カーネルは部分的なディスパッチによるパフォーマンスの恩恵を受けることはありません。したがって、work-group のサイズを適切に選択することが、この問題の回避につながります。

23

oneAPI GPU 最適化ガイド
次の表は、グループサイズ、スレッド数、Xe コアの利用率、および占有率間のトレードオフをまとめたものです。
各種構成での利用
work-item 数
64 x 64 x 128 =52428864 x 64 x 128 =52428864 x 64 x 128 =52428864 x 64 x 128 =52428864 x 64 x 128 =524288

グループサイズ(R=1) 128

スレッド数
16

Xe コア 利用率
16 / 112 = 14%

Xe コア 占有率
100% (7 work-group)(R=2) 128 x 2

2 x 16 = 32

32 / 112 = 28.6%

86% (3 work-group)(R=3) 128 x 4

3 x 16 = 48

49 / 112 = 42.9%

86% (2 work-group)(R=4) 128 x 4

4 x 16 = 64

64 / 112 = 57%

最大 57%(R>4) 640+

起動の失敗

work-group 内ローカルメモリーの影響
カーネルが work-group にローカルメモリーを割り当てる例を見てみましょう:
1.
2.
3.
4.
5.
6.
7.

auto command_group =[&](auto &cgh) {// ワーク項目間で共有されるローカルメモリー変数
sycl::accessor<int, 1, sycl::access::mode::read_write,
sycl::access::target::local>
myLocal(sycl::range(R), cgh);
cgh.parallel_for(nd_range(sycl::range<3>(64, 64, 128), // グローバルレンジ

8.
sycl::range<3>(1, R, 128)
9.
),
10.
[=](ngroup<3> myGroup) {11.
// (work-group コード)12.
myLocal[myGroup.get_local_id()[1]] = ...
13.
})
14. }

// ローカルレンジ

work-group のローカル変数は work-item 間で共有されるため、Xe コアの SLM に割り当てることができます。したがって、この work-group は、グループ内の同期と同様に単一の Xe コアに割り当てる必要があります。さらに、ローカ
ル変数が Xe コア SLM の 128KB 容量内に収まるよう、さまざまなグループサイズでローカル変数のサイズを比較して検討する必要があります。
詳細例
このセクションの最後では、単純なベクトル加算の例のバリエーションからハードウェアの占有率を検証してみます。
Xe LP (TGL✝) GPU で指定されるリソース・パラメーターで、ベースとなるハードウェアとしてインテル® Iris® Xe グラ
フィックス (TGL✝) を使用します。
1. auto d_selector = sycl::default_selector_v;
2.
3. // Array type and data size for this example.
4. constexpr size_t array_size = 3 * 5 * 7 * (1 << 17);
5. typedef std::array<int, array_size> IntArray;6.
7. #define mysize (1 << 17)8.
9. int VectorAdd1(sycl::queue &q, const IntArray &a, const IntArray &b,
10.
IntArray &sum, int iter) {11.
sycl::range num_items{a.size()};24

oneAPI GPU 最適化ガイド
12.
13.
sycl::buffer a_buf(a);14.
sycl::buffer b_buf(b);15.
sycl::buffer sum_buf(sum.data(), num_items);
16.
17.
auto start = std::chrono::steady_clock::now();
18.
auto e = q.submit([&](auto &h) {19.
// 入力アクセサー
20.
sycl::accessor a_acc(a_buf, h, sycl::read_only);21.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
22.
// 出力アクセサー
23.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
24.
25.
h.parallel_for(num_items, [=](auto i) {26.
for (int j = 0; j < iter; j++)27.
sum_acc[i] = a_acc[i] + b_acc[i];
28.
});
29.
});
30.
q.wait();31.
auto end = std::chrono::steady_clock::now();32.
std::cout << "VectorAdd1 completed on device - took " << (end - start).count()33.
<< " u-secs\n";34.
return ((end - start).count());
35. } // VectorAdd1 の終わり

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.

template <int groups>
int VectorAdd2(sycl::queue &q, const IntArray &a, const IntArray &b,
IntArray &sum, int iter) {sycl::range num_items{a.size()};sycl::buffer a_buf(a);sycl::buffer b_buf(b);sycl::buffer sum_buf(sum.data(), num_items);
size_t num_groups = groups;
size_t wg_size = 512;// get the max wg_sie instead of 512 size_t wg_size = 512;
auto start = std::chrono::steady_clock::now();
Q.submit([&](auto &h) {// Input accessors
sycl::accessor a_acc(a_buf, h, sycl::read_only);
sycl::accessor b_acc(b_buf, h, sycl::read_only);// Output accessor
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);h.parallel_for(sycl::nd_range(num_groups * wg_size, wg_size),[=](sycl::nd_item index) [[intel::reqd_sub_group_size(32)]] {size_t grp_id = index.get_group()[0];size_t loc_id = index.get_local_id();size_t start = grp_id * mysize;
size_t end = start + mysize;for (int j = 0; j < iter; j++)for (size_t i = start + loc_id; i < end; i += wg_size) {sum_acc[i] = a_acc[i] + b_acc[i];
}
});
});
q.wait();auto end = std::chrono::steady_clock::now();
std::cout << "VectorAdd2<" << groups << "> completed on device - took "<< (end - start).count() << " u-secs\n";return ((end - start).count());} // end VectorAdd2
void InitializeArray(IntArray &a) {25

oneAPI GPU 最適化ガイド
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.

89.
90.
91.
92.
93.
94.
95.
96.

for (size_t i = 0; i < a.size(); i++)a[i] = i;}void Initialize(IntArray &a) {for (size_t i = 0; i < a.size(); i++)a[i] = 0;}
IntArray a, b, sum;int main() {sycl::queue q(d_selector);
InitializeArray(a);InitializeArray(b);std::cout << "Running on device: "<< q.get_device().get_info() << "\n";std::cout << "Vector size: " << a.size() << "\n";// check results
Initialize(sum);
VectorAdd1(q, a, b, sum, 1);for (int i = 0; i < mysize; i++)if (sum[i] != 2 * i) {std::cout << "add1 Did not match\n";}Initialize(sum);
VectorAdd2(q, a, b, sum, 1);for (int i = 0; i < mysize; i++)if (sum[i] != 2 * i) {std::cout << "add2 Did not match\n";}// time the kernels
Initialize(sum);int t = VectorAdd1(q, a, b, sum, 1000);
Initialize(sum);t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
t = VectorAdd2(q, a, b, sum, 1000);
return 0;} // codeblock の終わり

上記の VectorAdd1 により、コンパイラーは work-group サイズと SIMD 幅を選択します。この場合、カーネルのレジスター・プレッシャーが低いことから、コンパイラーは work-group サイズ 512 と SIMD 幅 32 を選択します。
1.
2.
3.
4.
5.
6.
7.

26

int VectorAdd2(sycl::queue &q, const IntArray &a, const IntArray &b,
IntArray &sum, int iter) {sycl::range num_items{a.size()};sycl::buffer a_buf(a);sycl::buffer b_buf(b);sycl::buffer sum_buf(sum.data(), num_items);
size_t num_groups = groups;

oneAPI GPU 最適化ガイド
8.
size_t wg_size = 512;9.
// size_t wg_size = 512 の代わりに最大 wg_size 512 を取得
10.
11.
auto start = std::chrono::steady_clock::now();
12.
q.submit([&](auto &h) {13.
// 入力アクセサー
14.
sycl::accessor a_acc(a_buf, h, sycl::read_only);15.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
16.
// 出力アクセサー
17.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
18.
h.parallel_for(19.
sycl::nd_range<1>(num_groups * wg_size, wg_size),
20.
[=](sycl::nd_item<1> index) [[intel::reqd_sub_group_size(32)]] {21.
size_t grp_id = index.get_group()[0];22.
size_t loc_id = index.get_local_id();23.
size_t start = grp_id * mysize;24.
size_t end = start + mysize;25.
for (int j = 0; j < iter; j++)26.
for (size_t i = start + loc_id; i < end; i += wg_size) {27.
sum_acc[i] = a_acc[i] + b_acc[i];
28.
}
29.
});
30.
});
31.
q.wait();32.
auto end = std::chrono::steady_clock::now();33.
std::cout << "VectorAdd2<" << groups << "> completed on device - took "34.
<< (end - start).count() << " u-secs\n";35.
return ((end - start).count());
36. } // VectorAdd2 の終わり

上記の VectorAdd2 の例では、関数パラメーター・グループとして、work-group サイズ 512、SIMD 幅 32、および可変数の work-group を明示的に指定しています。
スレッド数を GPU で利用可能なスレッド・コンテキスト数で割ると、GPU ハードウェアの占有率を推測することができます。次の表は、さまざまな引数の組み合わせで前述の 2 つのカーネルに、インテル® Iris® Xe アーキテクチャー(TGL✝) パラメーターを指定して GPU ハードウェアの占有率を計算しています。
占有率プログラム占有率

workgroup 数

workitem 数

VectorAdd1
VectorAdd2<1>
VectorAdd2<2>
VectorAdd2<3>
VectorAdd2<4>
VectorAdd2<5>VectorAdd2<6>VectorAdd2<7>
VectorAdd2<8>VectorAdd2<12>
VectorAdd2<16>
VectorAdd2<20>
VectorAdd2<24>
VectorAdd2<28>
VectorAdd2<32>
VectorAdd2<36>
VectorAdd2<40>

53760
1
2
3
4
5
6
7
8
12
16
20
24
28
32
36
40

27.5M
512
1024
1536
2048
2560
3072
3584
4096
6144
8192
10240
12288
14336
16384
18432
20480

workgroup
サイズ
512
512
512
512
512
512
512
512
512
512
512
512
512
512
512
512
512

SIMD

32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32
32

workgroup の
スレッド数
16
16
16
16
16
16
16
16
16
16
16
16
16
16
16
16
16

スレッド数

占有率

860K
16
32
48
64
80
96
112
128
192
256
320
384
448
512
576
640

100%
16/672 = 2.4%
32/672 = 4.8%
48/672 = 7.1%
64/672 = 9.5%
80/672 = 11.9%
96/672 = 14.3%
112/672 = 16.7%
128/672 = 19%
192/672 = 28.6%
256/672 = 38.1%
320/672 = 47.7%
384/672 = 57.1%
448/672 = 66.7%
512/672 = 76.2%
576/672 = 85.7%
640/672 = 95.2%

27

oneAPI GPU 最適化ガイド
プログラム占有率

workgroup 数

workitem 数

VectorAdd2<42>
VectorAdd2<44>
VectorAdd2<48>

42
44
48

21504
22528
24576

workgroup
サイズ
512
512
512

SIMD

32
32
32

workgroup の
スレッド数
16
16
16

スレッド数

占有率

672
704
768

672/672 = 100%
100% 次に 4.7%
100% 次に 14.3%

次のインテル® VTune™ プロファイラーの VectorAdd2 のグラフで、さまざまな work-group サイズの予測精度を確認します。グリッドビューでは実行全体の平均が示されるため、グリッドビューの数値は予測と異なることがあります。
インテル® VTune™ プロファイラーで示される VectorAdd2 の占有率

次のタイムライン・ビューは一定期間の占有率を示しています。占有率メトリックは、カーネル実行の大部分で正確ですが、各スレッドが実行を終了する時間が異なるため後半に向かって減少していきます。
VectorAdd2 のタイムライン・ビュー

次に示す VectorAdd3 カーネルは上記のカーネルに似ていますが、2 つの点で異なります。
1.

テンプレート・パラメーターとして、work-group 数、work-group サイズ、および sub-group サイズをインスタンス化できます。これにより、スレッドの占有率に対する sub-group と work-group 数の影響を調査することができます。

2.

長時間のワークがエグゼキューション・ユニットに常駐するようにすることで、カーネル内のワーク量が劇的に増加します。
1.
2.
3.
4.
5.
6.
7.

28

template <int groups, int wg_size, int sg_size>
int VectorAdd3(sycl::queue &q, const IntArray &a, const IntArray &b,
IntArray &sum, int iter) {sycl::range num_items{a.size()};sycl::buffer a_buf(a);sycl::buffer b_buf(b);sycl::buffer sum_buf(sum.data(), num_items);

oneAPI GPU 最適化ガイド
8.
size_t num_groups = groups;
9.
auto start = std::chrono::steady_clock::now();
10.
q.submit([&](auto &h) {11.
// 入力アクセサー
12.
sycl::accessor a_acc(a_buf, h, sycl::read_only);13.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
14.
// 出力アクセサー
15.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
16.
h.parallel_for(17.
sycl::nd_range<1>(num_groups * wg_size, wg_size),18.
[=](sycl::nd_item<1> index) [[intel::reqd_sub_group_size(sg_size)]] {19.
size_t grp_id = index.get_group()[0];20.
size_t loc_id = index.get_local_id();21.
size_t start = grp_id * mysize;
22.
size_t end = start + mysize;23.
for (int j = 0; j < iter; j++)24.
for (size_t i = start + loc_id; i < end; i += wg_size) {25.
sum_acc[i] = a_acc[i] + b_acc[i];
26.
}
27.
});
28.
});
29.
q.wait();30.
auto end = std::chrono::steady_clock::now();31.
std::cout << "VectorAdd3<" << groups << "> completed on device - took "32.
<< (end - start).count() << " u-secs\n";33.
return ((end - start).count());
34. } // VectorAdd3 の終わり

VectorAdd4 カーネルは、カーネル実行の開始時と終了時にバリア同期があることを除いて、前述の VectorAdd3
カーネルに類似しています。このバリアは機能的には必要ありませんが、ハードウェアでスレッドをスケジュールする
のに大きく影響します。
1. template <int groups, int wg_size, int sg_size>2.
int VectorAdd4(sycl::queue &q, const IntArray &a, const IntArray &b,
3.
IntArray &sum, int iter) {4.
sycl::range num_items{a.size()};5.
sycl::buffer a_buf(a);6.
sycl::buffer b_buf(b);7.
sycl::buffer sum_buf(sum.data(), num_items);
8.
size_t num_groups = groups;
9.
auto start = std::chrono::steady_clock::now();
10.
q.submit([&](auto &h) {11.
// 入力アクセサー
12.
sycl::accessor a_acc(a_buf, h, sycl::read_only);13.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
14.
// 出力アクセサー
15.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
16.
h.parallel_for(17.
sycl::nd_range<1>(num_groups * wg_size, wg_size),18.
[=](sycl::nd_item<1> index) [[intel::reqd_sub_group_size(sg_size)]] {19.
index.barrier(sycl::access::fence_space::local_space);20.
size_t grp_id = index.get_group()[0];21.
size_t loc_id = index.get_local_id();22.
size_t start = grp_id * mysize;23.
size_t end = start + mysize;24.
for (int j = 0; j < iter; j++) {25.
for (size_t i = start + loc_id; i < end; i += wg_size) {26.
sum_acc[i] = a_acc[i] + b_acc[i];
27.
}
28.
}
29.
});
30.
});
31.
q.wait();32.
auto end = std::chrono::steady_clock::now();33.
std::cout << "VectorAdd4<" << groups << "> completed on device - took "29

oneAPI GPU 最適化ガイド34.
<< (end - start).count() << " u-secs\n";35.
return ((end - start).count());
36. } // VectorAdd4 の終わり

スレッドのスケジュール方法を説明するため、上記の 2 つのカーネルが、次のように 8 つの work-group、subgroup サイズ 8、および work-group サイズ 320 で呼び出されることを考えてみます。work-group サイズと subgroup サイズから、ハードウェアは work-group ごとに 40 スレッドをスケジュールします。
1.
2.
3.
4.

Initialize(sum);
VectorAdd3<8, 320, 8>(q, a, b, sum, 10000);
Initialize(sum);
VectorAdd4<8, 320, 8>(q, a, b, sum, 10000);

次のインテル® VTune™ プロファイラーによるグラフは、VectorAdd3 と VectorAdd4 カーネルで測定された占有率を示します。
VectorAdd3、VectorAdd4 カーネルの GPU 占有率

VectorAdd3 カーネルでは、占有率に 2 つのフェーズがあることが分かります。合計 672 スレッドの TGL✝ マシン
で、最初が 33.3% (224 スレッドを占有) で、その次は 14.3% (96 スレッドを占有） です。それぞれ 40 スレッドを持つ
合計 8 つの work-group があることが判明しているため、6 つの work-group のスレッドがスケジュールされる 2つのサブスライス (それぞれ 112 スレッド) があると考えることができます。これは、最初のフェーズでは、4 つのwork-group にそれぞれの 40 スレッドがスケジュールされ、2 つの work-group に 32 スレッドがスケジュールされていることを意味します。それに続く 2 番目のフェーズでは、残りの 2 つの work-group に 40 スレッドがスケ
ジュールされています。
VectorAdd4 カーネルでは、占有率は 45.3% (304 スレッド)、39.3% (264 スレッド)、および 11.9% (80 スレッド）の 3 つのフェーズがあります。最初のフェーズでは 8 つの work-group すべてが 3 つのサブスライスでスケジュール
されています。2 つのサブスライスにはそれぞれ 112 スレッド (2 つの work-group で 80、1 つで 32) が割り当てられ、1 つのサブスライスは 80 スレッドが割り当てられます (2 つの work-group から)。2 番目のフェーズでは、1 つのwork-group が実行を完了し、占有率は (304 - 40 = 264) となります。そして最後のフェーズで、2 つの work-groupの残りの 8 スレッドがスケジュールされ実行が完了します。
上記のカーネルをサブスライス内のスレッドがスレッド数の倍数になるように構成された、異なる work-group サイズで実行した場合、以下の図のようにより多くの work-group がハードウェアを十分に活用してほぼ 100% の占有率を達成することができます。
30

oneAPI GPU 最適化ガイド
1.
2.
3.
4.

Initialize(sum);
VectorAdd3<24, 224, 8>(q, a, b, sum, 10000);
Initialize(sum);
VectorAdd4<24, 224, 8>(q, a, b, sum, 10000);

このカーネルの実行は、スレッド数が多く、work-group サイズが Xe コア内のスレッド数の倍数であるため、スレッドの占有率は異なります。この様子を以下のインテル® VTune™ プロファイラーにおけるスレッド占有率のメトリックに示します。
インテル® VTune™ プロファイラーが示すスレッド占有率のメトリック:

スライスごとの占有率を調査する方法がまだないため、上記のスケジュールは、異なる占有率に基づく推測であることに注意してください。
上記のカーネルをさまざまな環境と設定の組み合わせで実行し、GPU ハードウェアがエグゼキューション・ユニットにソフトウェア・スレッドをどのようにスケジュールするか理解することができます。GPU ハードウェアを効率良く利用するには、work-group の数に加えて、work-group と sub-group のサイズに注意する必要があります。

5-2-4インテル® GPU の占有率計算

要約すると、SYCL* work-group は通常、Xe コアにディスパッチされ、work-group 内のすべての work-item は、
work-group 内のスレッドバリアとメモリーフェンスによる同期のため、Xe コアの同じ SLM を共有します。VE ALU、
SLM、およびスレッド・コンテキストが十分にあれば、複数の work-group を同じ Xe コアにディスパッチできます。
利用可能なすべての Xe コアを完全に利用することで、高いパフォーマンスを得ることができます。カーネルの GPU 占
有率に影響するパラメーターは、work-group サイズと SIMD sub-group サイズであり、これは work-group 内のス
レッド数を決定します。
インテル® GPU の占有率計算 (英語) を使用すると、指定されたカーネルとその work-group パラメーターに対するインテル® GPU の占有率を計算できます。

31

oneAPI GPU 最適化ガイド

5-3 カーネル
カーネルは oneAPI オフロードモデルの計算単位です。反復空間のカーネルを送信することで、指定するデータ・オブ
ジェクトに計算を適用することを要求します。
このセクションでは、カーネルのコーディング、送信、および実行に関連するトピックについて説明します。

















sub-group と SIMD ベクトル化
条件チェックを排除
レジスターとパフォーマンス
共有ローカルメモリー
ポインターのエイリアシングと restrict ディレクティブ
カーネル内のスレッド間での同期
work-group サイズを選択する考慮事項
プリフェッチ
リダクション
カーネルの起動
デバイス上で同時に複数のカーネルを実行
複数のキューへカーネルを送信
冗長なキューの構築を避ける
SYCL* 結合行列拡張を使用したインテル® XMX のプログラミング
カーネル内での I/O
明示的な SIMD カーネルの最適化

5-3-1

sub-group と SIMD ベクトル化

ND-Range カーネルのインデックス空間は、work-group、sub-group、および work-item に分割されます。workitem は基本単位です。work-item の集合は sub-group を形成し、sub-group の集合は work-group を形成します。work-item および work-group のハードウェアベクトルエンジン (VE) へのマッピングは、実装に依存します。
すべての work-group は同時に実行されますが、リソースの可用性に応じて異なるタイミングで実行するようにス
ケジュールされることもあります。work-group の実行は、ベースとなるハードウェアの能力に応じて、プリエンプト
される場合と、されない場合があります。同じ work-group 内の work-item は、同時に実行されることが保証されています。同じ sub-group 内の work-item は追加のスケジュールが保証されており、追加機能を利用できることがあります。
sub-group は、同じ VE スレッドで実行されるグローバル・インデックス空間で連続する work-item の集合です。デバ
イス・コンパイラーがカーネルをコンパイルする場合、複数の work-item がベクトル化によって sub-group にパックされるため、生成される SIMD 命令ストリームは複数の work-item を同時に実行できます。work-item を subgroup にパーティション化する場合、その粒度により大きなパフォーマンスの差が生じる可能性があります。
sub-group を示す簡単な例を見てみましょう。

32

oneAPI GPU 最適化ガイド
1. q.submit([&](auto &h) {2.
sycl::stream out(65536, 256, h);3.
h.parallel_for(sycl::nd_range(sycl::range{32}, sycl::range{32}),4.
[=](sycl::nd_item<1> it) {5.
int groupId = it.get_group(0);6.
int globalId = it.get_global_linear_id();7.
auto sg = it.get_sub_group();8.
int sgSize = sg.get_local_range()[0];9.
int sgGroupId = sg.get_group_id()[0];10.
int sgId = sg.get_local_id()[0];11.
12.
out << "globalId = " << sycl::setw(2) << globalId
13.
<< " groupId = " << groupId
14.
<< " sgGroupId = " << sgGroupId << " sgId = " << sgId
15.
<< " sgSize = " << sycl::setw(2) << sgSize
16.
<< sycl::endl;
17.
);18. });

この例の出力は次のようになります。
Device: Intel(R) Gen12HP
globalId = 0 groupId = 0 sgGroupId = 0 sgId = 0 sgSize = 16
globalId = 1 groupId = 0 sgGroupId = 0 sgId = 1 sgSize = 16
globalId = 2 groupId = 0 sgGroupId = 0 sgId = 2 sgSize = 16
globalId = 3 groupId = 0 sgGroupId = 0 sgId = 3 sgSize = 16
globalId = 4 groupId = 0 sgGroupId = 0 sgId = 4 sgSize = 16
globalId = 5 groupId = 0 sgGroupId = 0 sgId = 5 sgSize = 16
globalId = 6 groupId = 0 sgGroupId = 0 sgId = 6 sgSize = 16
globalId = 7 groupId = 0 sgGroupId = 0 sgId = 7 sgSize = 16
globalId = 16 groupId = 0 sgGroupId = 1 sgId = 0 sgSize = 16
globalId = 17 groupId = 0 sgGroupId = 1 sgId = 1 sgSize = 16
globalId = 18 groupId = 0 sgGroupId = 1 sgId = 2 sgSize = 16
globalId = 19 groupId = 0 sgGroupId = 1 sgId = 3 sgSize = 16
globalId = 20 groupId = 0 sgGroupId = 1 sgId = 4 sgSize = 16
globalId = 21 groupId = 0 sgGroupId = 1 sgId = 5 sgSize = 16
globalId = 22 groupId = 0 sgGroupId = 1 sgId = 6 sgSize = 16
globalId = 23 groupId = 0 sgGroupId = 1 sgId = 7 sgSize = 16
globalId = 8 groupId = 0 sgGroupId = 0 sgId = 8 sgSize = 16
globalId = 9 groupId = 0 sgGroupId = 0 sgId = 9 sgSize = 16
globalId = 10 groupId = 0 sgGroupId = 0 sgId = 10 sgSize = 16
globalId = 11 groupId = 0 sgGroupId = 0 sgId = 11 sgSize = 16
globalId = 12 groupId = 0 sgGroupId = 0 sgId = 12 sgSize = 16
globalId = 13 groupId = 0 sgGroupId = 0 sgId = 13 sgSize = 16
globalId = 14 groupId = 0 sgGroupId = 0 sgId = 14 sgSize = 16
globalId = 15 groupId = 0 sgGroupId = 0 sgId = 15 sgSize = 16
globalId = 24 groupId = 0 sgGroupId = 1 sgId = 8 sgSize = 16
globalId = 25 groupId = 0 sgGroupId = 1 sgId = 9 sgSize = 16
globalId = 26 groupId = 0 sgGroupId = 1 sgId = 10 sgSize = 16
globalId = 27 groupId = 0 sgGroupId = 1 sgId = 11 sgSize = 16
globalId = 28 groupId = 0 sgGroupId = 1 sgId = 12 sgSize = 16
globalId = 29 groupId = 0 sgGroupId = 1 sgId = 13 sgSize = 16
globalId = 30 groupId = 0 sgGroupId = 1 sgId = 14 sgSize = 16
globalId = 31 groupId = 0 sgGroupId = 1 sgId = 15 sgSize = 16

この例の sub-group は、work-item 数 16 または sub-group サイズ 16 を持っています。これは、各スレッドが同時に 16 個の work-item を実行し、32 個の work-item が 2 つの VE スレッドで実行されることを意味しています。
デフォルトでは、コンパイラーはデバイス固有の情報とヒューリスティックから sub-group サイズを決定します。ユー
ザーは、カーネル属性 intel::reqd_sub_group_size を使用してコンパイラーの選択をオーバーライドすることで、sub-group の最大サイズを変更できます。sub-group サイズを明示的に要求することでパフォーマンスが向上
することがあります。

33

oneAPI GPU 最適化ガイド
1. q.submit([&](auto &h) {2.
sycl::stream out(65536, 256, h);3.
h.parallel_for(sycl::nd_range(sycl::range{32}, sycl::range{32}),4.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(32)]] {5.
int groupId = it.get_group(0);6.
int globalId = it.get_global_linear_id();7.
auto sg = it.get_sub_group();8.
int sgSize = sg.get_local_range()[0];9.
int sgGroupId = sg.get_group_id()[0];10.
int sgId = sg.get_local_id()[0];11.
12.
out << "globalId = " << sycl::setw(2) << globalId
13.
<< " groupId = " << groupId
14.
<< " sgGroupId = " << sgGroupId << " sgId = " << sgId
15.
<< " sgSize = " << sycl::setw(2) << sgSize
16.
<< sycl::endl;
17.
});18. });

出力は次のようになります。
Device: Intel(R) Gen12HP
globalId = 0 groupId = 0 sgGroupId = 0 sgId = 0 sgSize = 32
globalId = 1 groupId = 0 sgGroupId = 0 sgId = 1 sgSize = 32
globalId = 2 groupId = 0 sgGroupId = 0 sgId = 2 sgSize = 32
globalId = 3 groupId = 0 sgGroupId = 0 sgId = 3 sgSize = 32
globalId = 4 groupId = 0 sgGroupId = 0 sgId = 4 sgSize = 32
globalId = 5 groupId = 0 sgGroupId = 0 sgId = 5 sgSize = 32
globalId = 6 groupId = 0 sgGroupId = 0 sgId = 6 sgSize = 32
globalId = 7 groupId = 0 sgGroupId = 0 sgId = 7 sgSize = 32
globalId = 8 groupId = 0 sgGroupId = 0 sgId = 8 sgSize = 32
globalId = 9 groupId = 0 sgGroupId = 0 sgId = 9 sgSize = 32
globalId = 10 groupId = 0 sgGroupId = 0 sgId = 10 sgSize = 32
globalId = 11 groupId = 0 sgGroupId = 0 sgId = 11 sgSize = 32
globalId = 12 groupId = 0 sgGroupId = 0 sgId = 12 sgSize = 32
globalId = 13 groupId = 0 sgGroupId = 0 sgId = 13 sgSize = 32
globalId = 14 groupId = 0 sgGroupId = 0 sgId = 14 sgSize = 32
globalId = 15 groupId = 0 sgGroupId = 0 sgId = 15 sgSize = 32
globalId = 16 groupId = 0 sgGroupId = 0 sgId = 16 sgSize = 32
globalId = 17 groupId = 0 sgGroupId = 0 sgId = 17 sgSize = 32
globalId = 18 groupId = 0 sgGroupId = 0 sgId = 18 sgSize = 32
globalId = 19 groupId = 0 sgGroupId = 0 sgId = 19 sgSize = 32
globalId = 20 groupId = 0 sgGroupId = 0 sgId = 20 sgSize = 32
globalId = 21 groupId = 0 sgGroupId = 0 sgId = 21 sgSize = 32
globalId = 22 groupId = 0 sgGroupId = 0 sgId = 22 sgSize = 32
globalId = 23 groupId = 0 sgGroupId = 0 sgId = 23 sgSize = 32
globalId = 24 groupId = 0 sgGroupId = 0 sgId = 24 sgSize = 32
globalId = 25 groupId = 0 sgGroupId = 0 sgId = 25 sgSize = 32
globalId = 26 groupId = 0 sgGroupId = 0 sgId = 26 sgSize = 32
globalId = 27 groupId = 0 sgGroupId = 0 sgId = 27 sgSize = 32
globalId = 28 groupId = 0 sgGroupId = 0 sgId = 28 sgSize = 32
globalId = 29 groupId = 0 sgGroupId = 0 sgId = 29 sgSize = 32
globalId = 30 groupId = 0 sgGroupId = 0 sgId = 30 sgSize = 32
globalId = 31 groupId = 0 sgGroupId = 0 sgId = 31 sgSize = 32

有効な sub-group のサイズはデバイスによって異なります。デバイスを照会して次の情報を得られます。
1.
2.
3.
4.
5.
6.

34

std::cout << "sub-group Sizes: ";for (const auto &s :q.get_device().get_info<sycl::info::device::sub_group_sizes>()) {std::cout << s << " ";}std::cout << std::endl;

oneAPI GPU 最適化ガイド
サポートされる有効な sub-group のサイズは以下です。
Subgroup Sizes: 8 16 32

次に、sub-group を使用してパフォーマンスを向上する方法を示します。

5-3-1-1

ベクトル化とメモリーアクセス

インテル® グラフィックス・デバイスは複数の VE を搭載しています。各 VE はマルチスレッド SIMD プロセッサーです。
コンパイラーは、VE スレッドで同時にされる sub-group に複数の work-item をパックする SIMD 命令を生成します。SIMD 幅 (sub-group サイズ) は、デバイスの特性とヒューリスティックに基づいてコンパイラーによって選択されるか、カーネルによって明示的に要求され、8、16、または 32 にできます。
指定する SIMD 幅で SIMD レーンの使用率を最大化すると、最適な命令パフォーマンスが得られます。1 つ以上のレーン (またはカーネル・インスタンスや work-item) に分かれている場合、スレッドはパスが後続でマージされる前に両方の分岐パスを実行するため、動的な命令数は増加します。SIMD の分散はパフォーマンスに影響します。コンパ
イラーは分散を最小限化しようとしますが、可能であればソースコードで分散を回避することが有益です。
work-item のメモリーアクセスは、sub-group のメモリーアクセスや SIMD レーンの使い方に影響します。workitem の連続したメモリーアクセスは、多くの場合最適とは言えません。次に例を示します。
1. constexpr int N = 1024 * 1024;
2. int *data = sycl::malloc_shared<int>(N, q);3.
4. auto e = q.submit([&](auto &h) {5.
h.parallel_for(sycl::nd_range(sycl::range{N / 16}, sycl::range{32}),6.
[=](sycl::nd_item<1> it) {7.
int i = it.get_global_linear_id();8.
i = i * 16;
9.
for (int j = i; j < (i + 16); j++) {10.
data[j] = -1;
11.
}
12.
});
13. });
14. q.wait();

この簡単なカーネルは 1024 x 1024 の整数配列を初期化します。各 work-item は連続した 16 個の整数を初期化します。コンパイラーが sub-group サイズに 16 を選択したと仮定すると、各 sub-group またはスレッドで 256 個の整数が初期化されます。ただし、SIMD レーン 16 のストアはスキャッターされます。
work-item で 16 個の連続する整数を繰り返し初期化する代わりに、単一の SIMD 命令で 16 個の連続する整数を一度に初期化するほうが効果的です。
1. constexpr int N = 1024 * 1024;
2. int *data = sycl::malloc_shared<int>(N, q);3.
4. auto e = q.submit([&](auto &h) {5.
h.parallel_for(sycl::nd_range(sycl::range{N / 16}, sycl::range{32}),6.
[=](sycl::nd_item<1> it) {7.
int i = it.get_global_linear_id();8.
auto sg = it.get_sub_group();9.
int sgSize = sg.get_local_range()[0];10.
i = (i / sgSize) * sgSize * 16 + (i % sgSize);11.
for (int j = 0; j < sgSize * 16; j += sgSize) {12.
data[i + j] = -1;
13.
}
14.
});
15. });

35

oneAPI GPU 最適化ガイド
この例では、メモリー書き込みを行いますが、同じ手法をメモリー読み取りにも適用できます。
1. constexpr int N = 1024 * 1024;
2. int *data = sycl::malloc_shared<int>(N, q);
3. int *data2 = sycl::malloc_shared<int>(N, q);
4. memset(data2, 0xFF, sizeof(int) * N);
5.
6. auto e = q.submit([&](auto &h) {7.
h.parallel_for(sycl::nd_range(sycl::range{N / 16}, sycl::range{32}),8.
[=](sycl::nd_item<1> it) {9.
int i = it.get_global_linear_id();10.
i = i * 16;11.
for (int j = i; j < (i + 16); j++) {12.
data[j] = data2[j];
13.
}
14.
});
15. });

このカーネルは、1024 x 1024 の整数配列を同じサイズの別の整数配列にコピーします。各 work-item は連続する
16 個の整数をコピーします。ただし、data2 からの読み取りはギャザーされ、data へのストアはスキャッターされます。work-item の代わりに各 sub-group で連即する整数を読み取って格納するようにコードを変更すると、より効率が高まります。
1. constexpr int N = 1024 * 1024;
2. int *data = sycl::malloc_shared<int>(N, q);
3. int *data2 = sycl::malloc_shared<int>(N, q);
4. memset(data2, 0xFF, sizeof(int) * N);
5.
6. auto e = q.submit([&](auto &h) {7.
h.parallel_for(sycl::nd_range(sycl::range{N / 16}, sycl::range{32}),8.
[=](sycl::nd_item<1> it) {9.
int i = it.get_global_linear_id();10.
auto sg = it.get_sub_group();11.
int sgSize = sg.get_local_range()[0];12.
i = (i / sgSize) * sgSize * 16 + (i % sgSize);13.
for (int j = 0; j < sgSize * 16; j += sgSize) {14.
data[i + j] = data2[i + j];
15.
}
16.
});
17. });

5-3-1-2

メモリー帯域幅の利用率を最大化

上記の例の work-item は、ループ反復ごとに 1 つの整数、または 4 バイトをロードおよびストアします (data[i +j] = data2[i + j];)。もしくは、ベクトル化されたメモリー操作では、64 バイトをロード/ストアします (グローバ
ルサイズは 16)。メモリー帯域幅は飽和状態ではありません。
各 work-item が一度のメモリー操作でロードまたはストアするデータのペイロードやサイズを増やすと、帯域幅の利用率が高まります。
1. constexpr int N = 1024 * 1024;
2. int *data = sycl::malloc_shared<int>(N, q);
3. int *data2 = sycl::malloc_shared<int>(N, q);
4. memset(data2, 0xFF, sizeof(int) * N);
5.
6. auto e = q.submit([&](auto &h) {7.
h.parallel_for(sycl::nd_range(sycl::range{N / 16}, sycl::range{32}),8.
[=](sycl::nd_item<1> it) {9.
int i = it.get_global_linear_id();10.
auto sg = it.get_sub_group();11.
int sgSize = sg.get_local_range()[0];12.
i = (i / sgSize) * sgSize * 16 + (i % sgSize) * 4;36

oneAPI GPU 最適化ガイド
13.
for (int j = 0; j < 4; j++) {14.
sycl::vec<int, 4> x; sycl::vec<int, 4> *q =15.
(sycl::vec<int, 4> *)(&(data2[i + j * sgSize * 4])); x = *q;16.
sycl::vec<int, 4> *r =17.
(sycl::vec<int, 4> *)(&(data[i + j * sgSize * 4])); *r = x;
18.
}
19.
});
20. });

各 work-item は、すべてのループ反復で整数に代わって sycl::vec<int, 4> をロード/ストアします。連続した256 バイトのメモリーリード/ライト (sub-group サイズ 16 と仮定) は、ベクトル化されることですべてのメモリー操
作のペイロードは 4 倍になります。
最大帯域幅はハードウェアによって異なります。インテル® VTune™ プロファイラーを使用して帯域幅を測定し、最適なサイズを特定できます。
ただし、ベクトルタイプを使用するとレジスターの負荷が増加する可能性があります。レジスタースピルが発生しない
場合に限り、長いベクトルを使用することを推奨します。レジスタースピルを回避する方法については、「レジスターとパフォーマンス」の節を参照してください。

5-3-1-3

メモリーのブロックロードとブロックストア

インテル® グラフィックスには、メモリーのブロックロード/ストアに最適化された命令が用意されています。subgroup 内の work-item が連続するメモリーブロックにアクセスする場合、sub-group ブロックのアクセス関数を使用して、ブロック単位でロード/ストア命令を利用できます。
1. constexpr int N = 1024 * 1024;
2. int *data = sycl::malloc_shared<int>(N, q);
3. int *data2 = sycl::malloc_shared<int>(N, q);
4. memset(data2, 0xFF, sizeof(int) * N);
5.
6. auto e = q.submit([&](auto &h) {7.
h.parallel_for(8.
sycl::nd_range(sycl::range{N / 16}, sycl::range{32}), [=](sycl::nd_item<1> it)9.
[[intel::reqd_sub_group_size(16)]] {10.
auto sg = it.get_sub_group();11.
sycl::vec<int, 8> x;
12.
13.
int base = (it.get_group(0) * 32 +14.
sg.get_group_id()[0] * sg.get_local_range()[0]) * 16;15.
16.
x = sg.load<8>(global_ptr(&(data2[base + 0])));
17.
sg.store<8>(global_ptr(&(data[base + 0])), x);18.
x = sg.load<8>(global_ptr(&(data2[base + 128])));
19.
sg.store<8>(global_ptr(&(data[base + 128])), x);
20.
});21. });

この例では、パフォーマンス向上のため sycl::vec を使用していますが、整数 x はメモリー内で連続していないた
め、sub-group サイズのストライドを使用しています。
sub-group サイズが 16 であることに気付きましたか? sub-group 関数を利用する場合、コンパイラーの選択をオー
バーライドして sub-group サイズを常に期待通りにすることを推奨します。

37

oneAPI GPU 最適化ガイド

5-3-1-4

データ共有

sub-group 内の work-item は同一スレッドで実行されるため、データが各 work-item でプライベートであってもwork-item 間で共有するほうが効率的です。また、sub-group でデータを共有するほうが、共有ローカルメモリーやSLM を使用して work-group 間でデータを共有するよりも効果的です。sub-group 内の work-item 間でデータを共有するもう一つの方法は、シャッフル関数を使用することです。
1. constexpr size_t BLOCK_SIZE = 16;
2. sycl::buffer<uint, 2> m(matrix.data(), sycl::range<2>(N, N));3.
4. auto e = q.submit([&](auto &h) {5.
sycl::accessor marr(m, h);6.
sycl::local_accessor<uint, 2> barr1(
7.
sycl::range<2>(BLOCK_SIZE, BLOCK_SIZE), h);8.
sycl::local_accessor<uint, 2> barr2(
9.
sycl::range<2>(BLOCK_SIZE, BLOCK_SIZE), h);10.
11.
h.parallel_for(12.
sycl::nd_range<2>(sycl::range<2>(N / BLOCK_SIZE, N),
13.
sycl::range<2>(1, BLOCK_SIZE)),
14.
[=](sycl::nd_item<2> it) [[intel::reqd_sub_group_size(16)]] {15.
int gi = it.get_group(0);16.
int gj = it.get_group(1);17.
18.
auto sg = it.get_sub_group();
19.
uint sgId = sg.get_local_id()[0];20.
21.
uint bcol[BLOCK_SIZE];
22.
int ai = BLOCK_SIZE * gi;
23.
int aj = BLOCK_SIZE * gj;
24.
25.
for (uint k = 0; k < BLOCK_SIZE; k++) {26.
bcol[k] = sg.load(marr.get_pointer() + (ai + k) * N + aj);
27.
}
28.
29.
uint tcol[BLOCK_SIZE];
30.
for (uint n = 0; n < BLOCK_SIZE; n++) {31.
if (sgId == n) {32.
for (uint k = 0; k < BLOCK_SIZE; k++) {33.
tcol[k] = sg.shuffle(bcol[n], k);
34.
}
35.
}
36.
}
37.
38.
for (uint k = 0; k < BLOCK_SIZE; k++) {39.
sg.store(marr.get_pointer() + (ai + k) * N + aj, tcol[k]);
40.
}
41.
});
42. });

このカーネルは 16 x 16 の行列を転置します。前述の例より複雑に見えますが、考え方は単純です。sub-group が 16
x 16 のサブ行列をロードして、シャッフル関数を使用してサブ行列を転置します。サブ行列は 1 つの行列であるため、必要となる sub-group は 1 つのみです。4096 x 4096 のような大きな行列も同様に転置できます。各 sub-groupはサブ行列をロードし、サブ行列は sub-group のシャッフル関数を使用して転置されます。これは演習として皆さんに任せますので、試してみてください。
SYCL* 使用できる sub-group シャッフル関数にはいくつかのバリエーションがあります。それぞれのバリアントは、
特定デバイスの特定用途向けに最適化されています。独自の関数を作成せずに、これらの最適化された関数を利用することを推奨します (要件に一致する場合)。

38

oneAPI GPU 最適化ガイド

5-3-1-5

sub-group サイズと最大 sub-group サイズ

ここまでの例では、work-group のサイズは sub-group のサイズと、work-group のサイズおよび sub-group のサ
イズの両方で割り切れます (2 の累乗)。work-group のサイズが最大 sub-group サイズで割り切れ、両方のサイズが2 の累乗である場合、sub-group サイズと最大 sub-group サイズは同じです。しかし、work-group サイズが subgroup サイズで割り切れない場合はどうでしょう? 次の例について考えてみます。
1. auto e = q.submit([&](auto &h) {2.
sycl::stream out(65536, 128, h);3.
h.parallel_for(sycl::nd_range<1>(7, 7),4.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {5.
int i = it.get_global_linear_id();6.
auto sg = it.get_sub_group();7.
int sgSize = sg.get_local_range()[0];8.
int sgMaxSize = sg.get_max_local_range()[0];9.
int sId = sg.get_local_id()[0];10.
int j = data[i];11.
int k = data[i + sgSize];
12.
out << "globalId = " << i << " sgMaxSize = " << sgMaxSize
13.
<< " sgSize = " << sgSize << " sId = " << sId
14.
<< " j = " << j << " k = " << k << sycl::endl;
15.
});16. });
17. q.wait();

この例の出力では次のようになります。
globalId = 0 sgMaxSize = 16 sgSize = 7 sId = 0 j = 0 k = 7
globalId = 1 sgMaxSize = 16 sgSize = 7 sId = 1 j = 1 k = 8
globalId = 2 sgMaxSize = 16 sgSize = 7 sId = 2 j = 2 k = 9
globalId = 3 sgMaxSize = 16 sgSize = 7 sId = 3 j = 3 k = 10
globalId = 4 sgMaxSize = 16 sgSize = 7 sId = 4 j = 4 k = 11
globalId = 5 sgMaxSize = 16 sgSize = 7 sId = 5 j = 5 k = 12
globalId = 6 sgMaxSize = 16 sgSize = 7 sId = 6 j = 6 k = 13

sub-group サイズは 7 ですが、最大 sub-group サイズは 16 のままです。sub-group の最大サイズは SIMD 幅であるため変更されませんが、sub-group 内の work-item は 8 未満であるため、sub-group のサイズは 7 となります。
そのため、work-group サイズが sub-group の最大サイズで割り切れない場合は注意が必要です。work-item が少ない最後の sub-group は、別の処理が必要な場合があります。

5-3-2

条件チェックを排除

sub-group と SIMD ベクトル化の項で、SIMD の分散がパフォーマンスに影響する可能性を説明しました。subgroup 内の work-item がすべて同じ命令を実行すると、SIMD レーンを最大限に活用できます。1 つ以上の workitem が分散パスで処理される場合、マージする前に両方のパスが実行される必要があります。
すべての条件チェックが分散を引き起こすわけではありませんが、通常、分散は条件チェックによって生じます。一部の条件チェックは、SIMD の分散を引き起こさなくてもパフォーマンス・ハザードとなる可能性があります。一般に、条件チェックを排除するとパフォーマンスの向上につながります。

39

oneAPI GPU 最適化ガイド

5-3-2-1

バッファーをパディングして条件チェックを排除

共有ローカルメモリーの畳み込みの例を見てみましょう。
1. sycl::buffer<int> ibuf(input.data(), N);
2. sycl::buffer<int> obuf(output.data(), N);
3. sycl::buffer<int> kbuf(kernel.data(), M);
4.
5. auto e = q.submit([&](auto &h) {6.
sycl::accessor iacc(ibuf, h, sycl::read_only);7.
sycl::accessor oacc(obuf, h);8.
sycl::accessor kacc(kbuf, h, sycl::read_only);
9.
10.
h.parallel_for(sycl::nd_range<1>(sycl::range{N}, sycl::range{256}),
11.
[=](sycl::nd_item<1> it) {12.
int i = it.get_global_linear_id();13.
int group = it.get_group()[0];14.
int gSize = it.get_local_range()[0];15.
16.
int t = 0;
17.
int _M = static_cast<int>(M);18.
int _N = static_cast<int>(N);19.
20.
if ((group == 0) || (group == _N / gSize - 1)) {21.
if (i < _M / 2) {22.
for (int j = _M / 2 - i, k = 0; j < _M; ++j, ++k) {23.
t += iacc[k] * kacc[j];
24.
}
25.
} else {26.
if (i + _M / 2 >= _N) {27.
for (int j = 0, k = i - _M / 2;j < _M / 2 + _N - i; ++j, ++k) {28.
t += iacc[k] * kacc[j];
29.
}
30.
} else {31.
for (int j = 0, k = i - _M / 2; j < _M; ++j, ++k) {32.
t += iacc[k] * kacc[j];
33.
}
34.
}
35.
}
36.
} else {37.
for (int j = 0, k = i - _M / 2; j < _M; ++j, ++k) {38.
t += iacc[k] * kacc[j];
39.
}
40.
}
41.
42.
oacc[i] = t;
43.
});44. });

入れ子になった if-then-else の条件チェックは、入力の最初と最後の 128 要素を処理するために必要です。これ
により、インデックスが範囲外となることはありません。入力配列の最初と最後に 0 を埋めると、次のように条件チェッ
クを安全に排除できます。
1. std::vector<int> input(N + M / 2 + M / 2);
2. std::vector<int> output(N);3. std::vector<int> kernel(M);4.
5. srand(2009);
6. for (size_t i = M / 2; i < N + M / 2; ++i) {7.
input[i] = rand();
8. }9.
10. for (size_t i = 0; i < M / 2; ++i) {11.
input[i] = 0;
12.
input[i + N + M / 2] = 0;40

oneAPI GPU 最適化ガイド
13. }
14.
15. for (size_t i = 0; i < M; ++i) {16.
kernel[i] = rand();
17. }
18.
19. {
20. sycl::buffer<int> ibuf(input.data(), N + M / 2 + M / 2);
21. sycl::buffer<int> obuf(output.data(), N);
22. sycl::buffer<int> kbuf(kernel.data(), M);
23.
24. auto e = q.submit([&](auto &h) {25.
sycl::accessor iacc(ibuf, h, sycl::read_only);26.
sycl::accessor oacc(obuf, h);27.
sycl::accessor kacc(kbuf, h, sycl::read_only);
28.
29.
h.parallel_for(sycl::nd_range(sycl::range{N}, sycl::range{256}),30.
[=](sycl::nd_item<1> it) {31.
int i = it.get_global_linear_id();32.
int t = 0;33.
34.
for (size_t j = 0; j < M; ++j) {35.
t += iacc[i + j] * kacc[j];
36.
}
37.
38.
oacc[i] = t;
39.
});40. });
41. q.wait();42.
43. size_t kernel_ns = (e.template get_profiling_info<
44.
sycl::info::event_profiling::command_end>() –45.
e.template get_profiling_info<sycl::info::event_profiling::command_start>());
46. std::cout << "Kernel Execution Time Average: total = "47.
<< kernel_ns * 1e-6
48.
<< " msec" << std::endl;
49. }5-3-2-2

条件チェックをリレーショナル関数に置き換える

条件チェックをリレーショナル関数 (特にビルトイン・リレーショナル関数) に置き換えることで排除することもできます。ビルトイン関数が提供される場合、それらの利用を推奨します。SYCL* は、select()、min()、max() など豊富
なビルトイン・リレーショナル関数セットを提供します。多くの場合、条件チェックをこれらの関数に置き換えることでパフォーマンスを向上できます。
畳み込みの例をもう一度考えてみましょう。if-then-else 条件チェックは、ビルトイン関数 min() と max() に置
き換えることができます。
1. sycl::buffer<int> ibuf(input.data(), N);
2. sycl::buffer<int> obuf(output.data(), N);
3. sycl::buffer<int> kbuf(kernel.data(), M);
4.
5. auto e = q.submit([&](auto &h) {6.
sycl::accessor iacc(ibuf, h, sycl::read_only);7.
sycl::accessor oacc(obuf, h);8.
sycl::accessor kacc(kbuf, h, sycl::read_only);
9.
10.
h.parallel_for(sycl::nd_range(sycl::range{N}, sycl::range{256}),
11.
[=](sycl::nd_item<1> it) {12.
int i = it.get_global_linear_id();13.
int t = 0;
14.
int startj = sycl::max<int>(M / 2 - i, 0);15.
int endj = sycl::min<int>(M / 2 + N - i, M);16.
int startk = sycl::max<int>(i - M / 2, 0);17.
for (int j = startj, k = startk; j < endj; j++, k++) {41

oneAPI GPU 最適化ガイド
18.
t += iacc[k] * kacc[j];
19.
}
20.
oacc[i] = t;
21.
});22. });

5-3-3

レジスターとパフォーマンス

レジスターはメモリー階層における最速のストレージです。データを可能な限り長くレジスターに留めることは、パフォーマンスにとって重要です。一方、レジスター数は限られており、メモリー空間よりもはるかに少ないデータしか保持できません。例えば、現世代のインテル® GPU には、XVE スレッドごとに 32 バイト幅の汎用レジスターが 128個備わっています。
コンパイラーはできるだけ多くの変数をレジスターに割り当てようとしますが、実行中の任意の時点では限られた数のレジスターに少数の変数セットをのみを割り当てることができます。すべての変数が同時に必要となるわけではなく、さまざまな変数のセットが処理経過とともに変わるため、レジスターは処理中にいろいろな変数を保持することになります。必要とするすべての変数をレジスターに保持できない場合、レジスターがスピル (溢れる) することがあります。
また、レジスターにある変数をメモリーに退避して、別の変数向けのスペースを確保する必要が生じる場合もあります。
SYCL* では、コンパイラーはレジスターに work-item のプライベート変数を割り当てます。sub-group 内のすべてのwork-item は、単一の XVE スレッドにパックされます。デフォルトでは、コンパイラーは SIMD 幅または sub-group
サイズを決定するヒューリスティックの 1 つとしてレジスター・プレッシャーを考慮します。レジスター・プレッシャーが高いと、sub-group サイズが小さくなる可能性があります。例えば、sub-group サイズが明示的に指定されていない
場合、レジスタースピルが発生するか特定の変数がレジスターに格納されないとサイズは 16 ではなく 8 になります。
sub-group サイズや SIMD 幅が、ハードウェアの持つ最大値でない場合、ハードウェアが十分に活用されていないと考えられます。レジスターのスピルがホットなループ内で発生すると、パフォーマンスが大幅に低下する可能性があります。変数をレジスターに格納していない場合、その変数へのアクセスによりメモリー・トラフィックが大幅に増加することがあります。
コンパイラーはレジスターのスピルを回避もしくは最小化しようとしますが、開発者が最適化することで、コンパイ
ラーの適切な判断に役立ち、多くの場合パフォーマンスに良い結果をもたらします。


レジスタースピルが発生するカーネルを検出
スモール・レジスター・モードとラージ・レジスター・モード
レジスタースピルの最適化
レジスター・プレッシャーの高いコードをインテル® マックス GPU へポーティング

5-3-3-1

レジスタースピルが発生するカーネルを検出

カーネルがコンパイルされレジスタースピルが発生する場合、コンパイラーは警告を出力します。
1.
2.
3.
4.
5.
6.
7.

$ icpx -fsycl -fsycl-targets=spir64_gen -Xsycl-target-backend "-device pvc"
Compilation from IR - skipping loading of FCL
warning: kernel _ZTSZ4mainEUlT_E0_
spilled around 396

8. Build succeeded.

42

compiled SIMD32 allocated 128 regs andapp.cpp

oneAPI GPU 最適化ガイド
ただし、カーネルにレジスターへの書き込みが要求されないプライベート変数が 1 つ以上ある場合、コンパイラーは警告を発しません。
オープンソース・ツール unitrace は、レジスタースピルのあるカーネルと、レジスターへの書き込みを要求されないプ
ライベート変数のあるカーネルの両方を報告します。さらに、事前コンパイルとジャストインタイム・コンパイルの両方で機能します。
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.

$ icpx -fsycl app.cpp$ unitrace -d ./a.out...
== L0 Backend ==Kernel, Calls,
main::{lambda(auto:1)#4},
913975360
main::{lambda(auto:1)#3},
76974560
main::{lambda(auto:1)#5},
42880
main::{lambda(auto:1)#7},
38720
main::{lambda(auto:1)#6},
39040
main::{lambda(auto:1)#1},
3305120
main::{lambda(auto:1)#2},
31360

Time(ns), Time(%), Average(ns),
Min(ns),Max(ns)100, 91349596800, 99.896,
913495968, 913304160,
100,

77196960,

0.084,

771969,

2080,

500,

4981120,0.005,

9962,

1440,

500,

4600000,

0.005,

9200,

1440,

500,

4590880,

0.005,

9181,

1440,

100,

3494400,

0.003,

34944,

1760,

100,

189280,0.000,

1892,

1440,

=== Kernel Properties ===Kernel, Private Memory Per Thread, Spill Memory Per Thread
main::{lambda(auto:1)#4},
16384,
0
main::{lambda(auto:1)#3},
0,8192
main::{lambda(auto:1)#5},0,0
main::{lambda(auto:1)#7},0,0
main::{lambda(auto:1)#6},0,0
main::{lambda(auto:1)#1},0,0
main::{lambda(auto:1)#2},0,0

スレッドごとのスピルメモリー (Spill Memory Per Thread) のバイト値が 0 以外の場合は、カーネルがレジス
ターをスピルしていることを示し、スレッドごとのプライベート・メモリー (Private Memory Per Thread) のバイ
ト値が 0 以外の場合は、カーネルにレジスターに書き込まれないプライベート変数が少なくとも 1 つあることを示します。
このツールは、デバイス上で実行されたカーネルのタイミング統計も報告します。これらの統計は、開発者がレジス
ター スピルのパフォーマンスへの影響を評価し、最適化するカーネルを優先順位するのに役立ちます。

5-3-3-2

スモール・レジスター・モードとラージ・レジスター・モード

インテル® データセンター GPU Max マックス・シリーズ製品は、スモール GRF モードとラージ GRF モードの 2 つのGRF モードをサポートしています。各 XVE には合計 64 KB のレジスター空間があります。スモール GRF モードでは、
1 つのハードウェア・スレッドが 128 個の GRF レジスターにアクセスできます (レジスターの幅は 64 バイト)。このモードでは、XVE ごとに 8 つのハードウェア・スレッドを使用できます。ラージ GRF モードでは、1 つのハードウェア・スレッドが 256 個の GRF レジスターにアクセスできます (各レジスターの幅は 64 バイト)。このモードでは、XVE ごとに 4 つのハードウェア・スレッドを使用できます。

43

oneAPI GPU 最適化ガイド
インテル® グラフィックス・コンパイラー (IGC) には、これらのモードを選択する、(1) コマンドラインと (2) カーネルごとに指定する 2 つの方法があります。この節では、ユーザーが SYCL* バックエンドと OpenMP* バックエンドに対し、この制御を行うガイドラインを提供します。

5-3-3-2-1

コマンドラインでの GRF モード指定-ftarget-register-alloc-mode=<arg> コンパイラー・オプションは、IGC グラフィックス・コンパイラーでGRF モードの選択をガイドする機能を提供します。<arg> の形式は、Device0:Mode0[,Device1:Mode1...] です。現在サポートされているデバイスは pvc のみです。サポートされているモードは次のとおりです。
default - 選択するレジスター・ファイル・モードについて IGC に指定しません。現在、IGC は指定がない場合に常にスモール・レジスター・ファイル・モードを選択します。
small - すべてのカーネルに対して IGC がスモール・レジスター・ファイル・モードを選択するように強制します。
large - すべてのカーネルに対して IGC がラージ・レジスター・ファイル・モードを選択するように強制します。
auto - IGC がヒューリスティックに基づいてカーネルごとにスモール/ラージ GRF モードを選択できるようにします。
このオプションが省略されると、IGC は、インテル® データセンターGPU マックス・シリーズでは Linux* のヒューリス
ティックに基づいてカーネルごとに GRF モードを選択し、それ以外の場合はスモール GRF モードを選択します。
OpenMP* - GRF モードの選択 (AOT)
以下に、OpenMP* バックエンドの AOT コンパイルで指定できるバックエンド・オプションの組み合わせを示します。
ここではソースファイルに test.cpp を使用していますが任意のプログラムに変更できます。
$ icpx -fiopenmp -fopenmp-targets=spir64_gen -Xopenmp-target-backend"-device pvc -options -ze-opt-large-register-file" test.cpp// IGC はすべてのカーネルに対しラージ GRF モードを強制します。
$ icpx -fiopenmp -fopenmp-targets=spir64_gen -Xopenmp-target-backend"-device pvc -options -ze-intel-enable-auto-large-GRF-mode" test.cpp// IGC は、コンパイラーのヒューリスティックにしたがって、カーネルごとにスモール GRF とラージ GRF モードを選択します。
$ icpx -fiopenmp -fopenmp-targets=spir64_gen -Xopenmp-target-backend "-device pvc"
test.cpp// IGC すべてのカーネルに対しスモール GRF モードを選択します。

OpenMP* - GRF モードの選択 (JIT)
OpenMP* バックエンドの JIT コンパイルでは、環境変数 LIBOMPTARGET_LEVEL0_COMPILATION_OPTIONS を使用してバックエンド・オプションを指定します。以下に利用可能なオプションを示します。
$ export LIBOMPTARGET_LEVEL0_COMPILATION_OPTIONS="-ze-opt-large-register-file"// IGC はすべてのカーネルに対しラージ GRF モードを強制します。
$ export LIBOMPTARGET_LEVEL0_COMPILATION_OPTIONS="-ze-intel-enable-auto-large-GRF-mode"44

oneAPI GPU 最適化ガイド// IGC は、コンパイラーのヒューリスティックにしたがって、カーネルごとにスモール GRF とラージ GRF モードを選択します。
env-var not set// IGC すべてのカーネルに対しスモール GRF モードを選択します。

SYCL* - GRF モードの選択 (AOT)
以下に、SYCL* バックエンドの AOT コンパイルで指定できるバックエンド・オプションの組み合わせを示します。ここではソースファイルに test.cpp を使用していますが任意の SYCL* プログラムに変更できます。
$ icpx -fsycl -fsycl-targets=spir64_gen -Xsycl-target-backend"-device pvc -options -ze-opt-large-register-file" test.cpp// IGC はすべてのカーネルに対しラージ GRF モードを強制します。
$ icpx -fsycl -fsycl-targets=spir64_gen -Xsycl-target-backend"-device pvc -options -ze-intel-enable-auto-large-GRF-mode" test.cpp// IGC は、コンパイラーのヒューリスティックにしたがって、カーネルごとにスモール GRF とラージ GRF モードを選択します。
$ icpx -fsycl -fsycl-targets=spir64_gen -Xsycl-target-backend "-device pvc" test.cpp// IGC すべてのカーネルに対しスモール GRF モードを選択します。

SYCL* - GRF モードの選択 (JIT)
以下に、SYCL* バックエンドの JIT コンパイルで指定できるバックエンド・オプションの組み合わせを示します。ここではソースファイルに test.cpp を使用していますが任意の SYCL* プログラムに変更できます。
icpx -fsycl -fsycl-targets=spir64 -Xsycl-target-backend"-options -ze-opt-large-register-file" test.cpp// IGC はすべてのカーネルに対しラージ GRF モードを強制します。
icpx -fsycl -fsycl-targets=spir64 -Xsycl-target-backend"-options -ze-intel-enable-auto-large-GRF-mode" test.cpp// IGC は、コンパイラーのヒューリスティックにしたがって、カーネルごとにスモール GRF とラージ GRF モードを選択します。
icpx -fsycl -fsycl-targets=spir64 -Xsycl-target-backend "" test.cpp// IGC は、すべてのカーネルに対しスモール GRF モードを選択します。

5-3-3-2-2

GRF モード選択を使用したパフォーマンス・チューニング

この節では、GRF モードの選択がデバイスコードのパフォーマンスに与える影響について説明します。ここでは、OpenMP* オフロード機能を使用した、JIT コンパイルを使用します。GRF モードを制御することで、(1) カーネルコー
ドのレジスター・プレッシャー、と (2) 並列実行スレッド数、を考慮できます。次に OpenMP* オフロード領域を含むコードの例を示します。
1.
2.
3.
4.
5.
6.

#pragma omp target teams distribute thread_limit(ZDIM *NX1 *NX1)
for (int e = 0; e < nelt; e++) {double s_u[NX1 * NX1 * NX1];
double s_D[NX1 * NX1];// SLM は 3 つの配列で使用されます
double s_ur[NX1 * NX1 * NX1];

45

oneAPI GPU 最適化ガイド
7.
double s_us[NX1 * NX1 * NX1];
8.
double s_ut[NX1 * NX1 * NX1];
9. #pragma omp parallel for10.
for (int inner = 0; inner < innerub; inner++) {11.
int k = inner / (NX1 * NX1);
12.
int j = (inner - k * NX1 * NX1) / NX1;
13.
int i = inner - k * NX1 * NX1 - j * NX1;
14.
if (k == 0)15.
s_D[I2(i, j)] = D[I2(i, j)];16.
for (; k < NX1; k += ZDIM) {17.
s_u[I3(i, j, k)] = u[I4(i, j, k, e)];
18.
}
19.
}
20. #pragma omp parallel for21.
for (int inner = 0; inner < innerub; inner++) {22.
int k = inner / (NX1 * NX1);
23.
int j = (inner - k * NX1 * NX1) / NX1;
24.
int i = inner - k * NX1 * NX1 - j * NX1;
25.
double r_G00, r_G01, r_G02, r_G11, r_G12, r_G22;
26.
for (; k < NX1; k += ZDIM) {27.
double r_ur, r_us, r_ut;
28.
r_ur = r_us = r_ut = 0;
29. #ifdef FORCE_UNROLL
30. #pragma unroll NX1
31. #endif32.
for (int m = 0; m < NX1; m++) {33.
r_ur += s_D[I2(i, m)] * s_u[I3(m, j, k)];34.
r_us += s_D[I2(j, m)] * s_u[I3(i, m, k)];35.
r_ut += s_D[I2(k, m)] * s_u[I3(i, j, m)];
36.
}
37.
const unsigned gbase = 6 * I4(i, j, k, e);38.
r_G00 = g[gbase + 0];
39.
r_G01 = g[gbase + 1];
40.
r_G02 = g[gbase + 2];41.
s_ur[I3(i, j, k)] = r_G00 * r_ur + r_G01 * r_us + r_G02 * r_ut;
42.
r_G11 = g[gbase + 3];
43.
r_G12 = g[gbase + 4];44.
s_us[I3(i, j, k)] = r_G01 * r_ur + r_G11 * r_us + r_G12 * r_ut;
45.
r_G22 = g[gbase + 5];46.
s_ut[I3(i, j, k)] = r_G02 * r_ur + r_G12 * r_us + r_G22 * r_ut;
47.
}
48.
}
49. #pragma omp parallel for50.
for (int inner = 0; inner < innerub; inner++) {51.
int k = inner / (NX1 * NX1);
52.
int j = (inner - k * NX1 * NX1) / NX1;
53.
int i = inner - k * NX1 * NX1 - j * NX1;
54.
for (; k < NX1; k += ZDIM) {55.
double wr = 0.0;56.
for (int m = 0; m < NX1; m++) {57.
double s_D_i = s_D[I2(m, i)];58.
double s_D_j = s_D[I2(m, j)];59.
double s_D_k = s_D[I2(m, k)];60.
wr += s_D_i * s_ur[I3(m, j, k)] + s_D_j * s_us[I3(i, m, k)] +61.
s_D_k * s_ut[I3(i, j, m)];
62.
}
63.
w[I4(i, j, k, e)] = wr;
64.
}
65.
}
66. }

46

oneAPI GPU 最適化ガイド
ここで変更可能なパラメーターは 2 つあります。(1) 行番号 30 にある内部ループのアンロール係数 (2) 行番号 1 にある OpenMP* チーム数アンロール係数を変更することでレジスター・プレッシャーを制御できます。アンロール係数を大きくすると、レジスターへのプレッシャーは高くなります。OpenMP* チーム数を変更することで、並列スレッド数を制御できます。ここでは、デバイス上でのカーネル実行時間をパフォーマンスの指標とします。ユーザーの環境やデバイスの設定よって異なるため、ここでは実際の数値は示しません。以下に、観察された結果を示します。
•

アンロールを無効にすると、スモール GRF モードでパフォーマンスが向上することが分かりました。これは、ラージ GRF モードのメリットを得るほど、レジスター・プレッシャーが高くないことを意味します。

•

アンロールを有効にすると、ラージ GRF モードでパフォーマンスが向上することが分かりました。これは、レジスター・プレッシャーが高く、ラージ GRF モードがプレッシャーの軽減に有効であることを意味します。

•

スモール GRF モードを使用すると、チーム数が増加し、ワークロードが大きくなる (プレッシャーが高まる)
場合、パフォーマンスが向上する傾向があります。

5-3-3-3

レジスタースピルの最適化

レジスター・プレッシャーを減らすにはいくつかの方法があります。
•

値を変数にロードしてからその変数を使用するまでの距離をなるべく短くします。
コンパイラーは命令をスケジュールして距離を最適化しますが、同じ変数へのロードと使用を近接させたり、ソース内の依存関係を排除することで、コンパイラーの最適化に有効な作用をもたらすことがあります。

•

過度なループアンロールを回避します。
ループアンロールは、コンパイラーによる命令スケジュールの最適化の可能性を高め、パフォーマンスの向上に
つながります。しかし、アンロールによって使用される一時変数がレジスター割り当てのプレッシャーを高め、レジ
スタースピルを引き起こす可能性があります。ループアンロールの有無、およびアンロールの回数を比較して、ループをアンロールするかまたその回数を決定してください。

•

USM ポインターを推奨します。
バッファーアクセサーは、USM ポインターよりも多くのスペースを使用します。ＵＳＭ ポインターとバッファーアクセサーを選択可能な場合は、ＵＳＭ ポインターを選択します。

•

低いコストで求められる値はその都度計算します。そうしないと、レジスターに長期間保持されることになります。

•

大きな配列や構造体の利用は避けます。大きな構造体は小さな複数の構造体に分割します。
例えば、
sycl::float4:
sycl::float4 v[8];が 4 つの float 配列、
float x[8]、float y[8]、float z[8]、float w[8]に分割可能であれば、float の 4 つの配列のすべて、または一部が sycl::float4 の配列よりもレジスターに割り当てられる可能性が高くなります。
•

大きなループを複数の小さなループに分解して、同時に利用される変数の数を減らすことができます。

•

可能な限り小さなデータタイプを使用します。

•

プライベート変数を volatile として宣言してはなりません。

•

sub-group でレジスターを共有します。

•

可能であれば、sub-group ブロックのロード/ストアを使用します。

•

共有ローカルメモリーを使用します。

47

oneAPI GPU 最適化ガイド
上記はすべてを網羅しているわけではありません。
この章の残りでは、上記の手法の特に最後の 5 つを適用する例を示します。

5-3-3-3-1

小さなデータタイプを選択

constexpr int BLOCK_SIZE = 256;
constexpr int NUM_BINS = 32;
std::vector<unsigned long> hist(NUM_BINS, 0);
sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);
auto e = q.submit([&](auto &h) {sycl::accessor macc(mbuf, h, sycl::read_only);
auto hacc = hbuf.get_access<sycl::access::mode::atomic>(h);h.parallel_for(sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {int group = it.get_group()[0];int gSize = it.get_local_range()[0];auto sg = it.get_sub_group();int sgSize = sg.get_local_range()[0];int sgGroup = sg.get_group_id()[0];unsigned long histogram[NUM_BINS]; // 分布図の値域は、レジスターに格納するには// ストレージを消費しすぎますfor (int k = 0; k < NUM_BINS; k++) {histogram[k] = 0;}
for (int k = 0; k < BLOCK_SIZE; k++) {unsigned long x =sg.load(macc.get_pointer() + group * gSize * BLOCK_SIZE +sgGroup * sgSize * BLOCK_SIZE + sgSize * k);#pragma unroll
for (int i = 0; i < 8; i++) {unsigned int c = x & 0x1FU;
histogram[c] += 1;
x = x >> 8;}}
for (int k = 0; k < NUM_BINS; k++) {hacc[k].fetch_add(histogram[k]);
}
});
});

この例では 32 値域の分布図を計算します。各 work-item には、unsigned long データタイプの 32 個のプライベート値域があります。大きなストレージが必要であるため、プライベート値域をレジスターに格納できずパフォーマ
ンスが低下します。
BLOCK_SIZE が 256 である場合、各プライベート分布図の値域の最大値は、unsigned int を超えることはありません。プライベート分布図の値域を unsigned long から unsigned int にしてレジスター・プレッシャーを減らし、プライベート値域をレジスターに収めることができます。これは簡単な変更ですがパフォーマンスに大きな影響を
与えます。
1.
2.
3.
4.

48

constexpr int BLOCK_SIZE = 256;
constexpr int NUM_BINS = 32;
std::vector<unsigned long> hist(NUM_BINS, 0);oneAPI GPU 最適化ガイド
5.
6. sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
7. sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);8.
9. auto e = q.submit([&](auto &h) {10.
sycl::accessor macc(mbuf, h, sycl::read_only);
11.
auto hacc = hbuf.get_access<sycl::access::mode::atomic>(h);12.
h.parallel_for(13.
sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),14.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {15.
int group = it.get_group()[0];16.
int gSize = it.get_local_range()[0];17.
auto sg = it.get_sub_group();18.
int sgSize = sg.get_local_range()[0];19.
int sgGroup = sg.get_group_id()[0];20.
21.
unsigned int histogram[NUM_BINS]; //分布図の値域はデータタイプが小さいほど
22.
//ストレージの使用量が少なくなります23.
for (int k = 0; k < NUM_BINS; k++) {24.
histogram[k] = 0;
25.
}
26.
for (int k = 0; k < BLOCK_SIZE; k++) {27.
unsigned long x =28.
sg.load(macc.get_pointer() + group * gSize * BLOCK_SIZE +29.
sgGroup * sgSize * BLOCK_SIZE + sgSize * k);
30. #pragma unroll
31.
for (int i = 0; i < 8; i++) {32.
unsigned int c = x & 0x1FU;
33.
histogram[c] += 1;
34.
x = x >> 8;
35.
}
36.
}
37.
38.
for (int k = 0; k < NUM_BINS; k++) {39.
hacc[k].fetch_add(histogram[k]);
40.
}
41.
});
42. });

5-3-3-3-2

プライベート変数を volatile として宣言しない

前述のコード例を少し変更します。
1.
2.

constexpr int BLOCK_SIZE = 256;
constexpr int NUM_BINS = 32;

3.

std::vector<unsigned long> hist(NUM_BINS, 0);

4.
5.

sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);

6. auto e = q.submit([&](auto &h) {7.
sycl::accessor macc(mbuf, h, sycl::read_only);
8.
auto hacc = hbuf.get_access<sycl::access::mode::atomic>(h);9.
h.parallel_for(sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),10.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {11.
int group = it.get_group()[0];12.
int gSize = it.get_local_range()[0];13.
auto sg = it.get_sub_group();14.
int sgSize = sg.get_local_range()[0];15.
int sgGroup = sg.get_group_id()[0];
16.
17.
18.

volatile unsigned int
histogram[NUM_BINS]; // volatile variables will not// be assigned to any registers

49

oneAPI GPU 最適化ガイド
19.
20.
21.
22.
23.
24.
25.
26. #pragma unroll
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37. });for (int k = 0; k < NUM_BINS; k++) {histogram[k] = 0;}
for (int k = 0; k < BLOCK_SIZE; k++) {unsigned long x = sg.load(
macc.get_pointer() + group * gSize * BLOCK_SIZE +sgGroup * sgSize * BLOCK_SIZE + sgSize * k);for (int i = 0; i < 8; i++) {unsigned int c = x & 0x1FU;
histogram[c] += 1;
x = x >> 8;}}
for (int k = 0; k < NUM_BINS; k++) {hacc[k].fetch_add(histogram[k]);}});プライベート histogram 配列は、volatile 配列として修飾されています。volatile 変数は揮発性であり、2 つの異なるロード操作間で値が変化する可能性があるため、レジスターには保持されません。
実際には、histogram のプライベート配列が volatile である理由はありません。プライベート変数が揮発性であ
る必要があるのであれば、それはもはやプライベートではありません。

5-3-3-3-3

プライベート変数のアドレスを取得し後にポインター参照しない

ここで、コード例にさらに変更を加えます。
1. constexpr int BLOCK_SIZE = 256;
2. constexpr int NUM_BINS = 32;
3.
4. std::vector<unsigned long> hist(NUM_BINS, 0);5.
6. sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
7. sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);8.
9. auto e = q.submit([&](auto &h) {10.
sycl::accessor macc(mbuf, h, sycl::read_only);
11.
auto hacc = hbuf.get_access<sycl::access::mode::atomic>(h);12.
h.parallel_for(13.
sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),14.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {15.
int group = it.get_group()[0];16.
int gSize = it.get_local_range()[0];17.
auto sg = it.get_sub_group();18.
int sgSize = sg.get_local_range()[0];19.
int sgGroup = sg.get_group_id()[0];20.
21.
unsigned int histogram[NUM_BINS]; // histogram bins take less storage
22.
// with smaller data type23.
for (int k = 0; k < NUM_BINS; k++) {24.
histogram[k] = 0;
25.
}
26.
for (int k = 0; k < BLOCK_SIZE; k++) {27.
unsigned long x =28.
sg.load(macc.get_pointer() + group * gSize * BLOCK_SIZE +29.
sgGroup * sgSize * BLOCK_SIZE + sgSize * k);30.
unsigned long *p = &x;
31. #pragma unroll
32.
for (int i = 0; i < 8; i++) {33.
unsigned int c = (*p & 0x1FU);
34.
histogram[c] += 1;

50

oneAPI GPU 最適化ガイド
35.
36.
37.
38.
39.
40.
41.
42.
43. });*p = (*p >> 8);}}
for (int k = 0; k < NUM_BINS; k++) {hacc[k].fetch_add(histogram[k]);}});プライベート変数 x のアドレスが取得され、ポインター p に格納され、その後 p が逆参照されて x にアクセスします。
そのアドレスが使用されるため、レジスターに空きがあっても変数 x はメモリー内に存在する必要があります。

5-3-3-3-4

sub-group でレジスターを共有

次に分布図の値域を 256 に増やします。
1. constexpr int BLOCK_SIZE = 256;
2. constexpr int NUM_BINS = 256;3.
4. std::vector<unsigned long> hist(NUM_BINS, 0);5.
6. sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
7. sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);8.
9. auto e = q.submit([&](auto &h) {10.
sycl::accessor macc(mbuf, h, sycl::read_only);
11.
auto hacc = hbuf.get_access<sycl::access::mode::atomic>(h);12.
h.parallel_for(13.
sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),14.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {15.
int group = it.get_group()[0];16.
int gSize = it.get_local_range()[0];17.
auto sg = it.get_sub_group();18.
int sgSize = sg.get_local_range()[0];19.
int sgGroup = sg.get_group_id()[0];20.
21.
unsigned int histogram[NUM_BINS]; // 分布図の値域は、レジスターに格納するには
22.
// ストレージを消費しすぎます23.
for (int k = 0; k < NUM_BINS; k++) {24.
histogram[k] = 0;
25.
}
26.
for (int k = 0; k < BLOCK_SIZE; k++) {27.
unsigned long x =28.
sg.load(macc.get_pointer() + group * gSize * BLOCK_SIZE +29.
sgGroup * sgSize * BLOCK_SIZE + sgSize * k);
30. #pragma unroll
31.
for (int i = 0; i < 8; i++) {32.
unsigned int c = x & 0x1FU;
33.
histogram[c] += 1;
34.
x = x >> 8;
35.
}
36.
}
37.
38.
for (int k = 0; k < NUM_BINS; k++) {39.
hacc[k].fetch_add(histogram[k]);
40.
}
41.
});
42. });

51

oneAPI GPU 最適化ガイド
256 個の分布図値域では、データタイプが unsigned int でもパフォーマンスは低下します。各 work-item のプラ
イベート値域のストレージは、レジスターに格納するには大きすぎます。
各 work-item が 256 個のプライベート分布図値域を持つ

sub-group サイズが 16 である場合、16 個の work-item が 1 つのベクトルエンジン (VE) のスレッドにパックされていることが分かります。また、同じ sub-group の work-item は相互に効率良く通信およびデータ共有できることが判明しています。同じ sub-group 内の work-item がプライベート分布図の値域を供する場合、sub-group 全体
で 256 個のプライベート値域が、または work-item には 16 個のプライベート値域が必要になります。
sub-group が 256 個のプライベート分布図値域を持つ

52

oneAPI GPU 最適化ガイド
sub-group の分布図値域を共有するには、各 work-item は入力データを同じ sub-group のすべての work-itemにブロードキャストします。対応する分布図値域を所有する work-item が更新を行います。
1. constexpr int BLOCK_SIZE = 256;
2. constexpr int NUM_BINS = 256;3.
4. std::vector<unsigned long> hist(NUM_BINS, 0);5.
6. sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
7. sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);8.
9. auto e = q.submit([&](auto &h) {10.
sycl::accessor macc(mbuf, h, sycl::read_only);
11.
auto hacc = hbuf.get_access<sycl::access::mode::atomic>(h);12.
h.parallel_for(13.
sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),14.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {15.
int group = it.get_group()[0];16.
int gSize = it.get_local_range()[0];17.
auto sg = it.get_sub_group();18.
int sgSize = sg.get_local_range()[0];19.
int sgGroup = sg.get_group_id()[0];20.
21.
unsigned int histogram[NUM_BINS / 16]; // 分布図はレジスターに格納するには
22.
// ストレージを消費しすぎます23.
for (int k = 0; k < NUM_BINS / 16; k++) {24.
histogram[k] = 0;
25.
}
26.
for (int k = 0; k < BLOCK_SIZE; k++) {27.
unsigned long x =28.
sg.load(macc.get_pointer() + group * gSize * BLOCK_SIZE +29.
sgGroup * sgSize * BLOCK_SIZE + sgSize * k);
30. // subgroup サイズは 16
31. #pragma unroll
32.
for (int j = 0; j < 16; j++) {33.
unsigned long y = sycl::group_broadcast(sg, x, j);
34. #pragma unroll
35.
for (int i = 0; i < 8; i++) {36.
unsigned int c = y & 0xFF;
37.
// (c & 0xF) は値域が存在する work-item です。
38.
// (c >> 4) は値域のインデックスです。
39.
if (sg.get_local_id()[0] == (c & 0xF)) {40.
histogram[c >> 4] += 1;
41.
}
42.
y = y >> 8;
43.
}
44.
}
45.
}
46.
47.
for (int k = 0; k < NUM_BINS / 16; k++) {48.
hacc[16 * k + sg.get_local_id()[0]].fetch_add(histogram[k]);
49.
}
50.
});
51. });

53

oneAPI GPU 最適化ガイド

5-3-3-3-5

sub-goup ブロックのロード/ストアを使用する

メモリーブロックのロード/ストアはベクトル化されます。ベクトルロード/ストア命令の各レーンには、固有のアドレ
スとデータがあります。アドレスとデータの両方がレジスター空間を持ちます。次に例を示します。
1. constexpr int N = 1024 * 1024;
2. int *data = sycl::malloc_shared<int>(N, q);
3. int *data2 = sycl::malloc_shared<int>(N, q);
4. memset(data2, 0xFF, sizeof(int) * N);
5.
6. auto e = q.submit([&](auto &h) {7.
h.parallel_for(sycl::nd_range(sycl::range{N}, sycl::range{32}),8.
[=](sycl::nd_item<1> it) {9.
int i = it.get_global_linear_id();10.
data[i] = data2[i];11.
});12. });

メモリーは、data[i] = data2[i]; 文でロードおよびストアされ、各ベクトルレーンは独自のアドレスを持ちます。SIMD 幅または sub-group サイズが 16 であると仮定すると、16 レーンのアドレスの合計レジスター空間は、
128 バイトになります。各 GRF レジスターが 32 バイト幅である場合、アドレスには 4 つの GRF レジスターが必要です。
アドレスが連続しているため。sub-group ブロックのロード/ストアのビルトインを使用することで、アドレスレジス
ター空間を節約できます。
1. constexpr int N = 1024 * 1024;
2. int *data = sycl::malloc_shared<int>(N, q);
3. int *data2 = sycl::malloc_shared<int>(N, q);
4. memset(data2, 0xFF, sizeof(int) * N);
5.
6. auto e = q.submit([&](auto &h) {7.
h.parallel_for(8.
sycl::nd_range(sycl::range{N}, sycl::range{32}),9.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {10.
auto sg = it.get_sub_group();11.
12.
int base = (it.get_group(0) * 32 +13.
sg.get_group_id()[0] * sg.get_local_range()[0]);14.
auto load_ptr = get_multi_ptr(&(data2[base + 0]));15.
int x = sg.load(load_ptr);
16.
17.
auto store_ptr = get_multi_ptr(&(data[base + 0]));18.
sg.store(store_ptr, x);
19.
});
});

ステートメントは以下です。
1.
2.

x = sg.load(global_ptr(&(data2[base + 0])));
sg.store(global_ptr(&(data[base + 0])), x);

上記の文は、連続したメモリーブロックをロード/ストアしますが、コンパイラーはこの 2 つの文を特別なメモリーブ
ロックのロード/ストア命令にコンパイルします。連続したメモリーブロックであるため、必要となるのはブロックの開始アドレスのみです。これにより、レジスター空間の 128 バイトではなく 8 バイト、または最大で 1 つのレジスターが各ブロックのロード/ストアのアドレスとして使用されます。

54

oneAPI GPU 最適化ガイド

5-3-3-3-6

共有ローカルメモリーを使用

例えば、分布図値域を 1024 に増やすと、プライベート値域が同じ sub-group で共有されていても、プライベート値
域向けのレジスターが足りなくなります。メモリー・トラフィックを軽減するには、ローカル分布図値域を共有ローカル
メモリーに割り当てて、同じ work-group 内の work-item で共有します。「共有ローカルメモリー」の項で分布図の例を確認してください。

5-3-3-4

レジスター・プレッシャーの高いコードをインテル® データセンター GPU マック
ス・シリーズへポーティング

コードを CUDA* から SYCL* に移行すると、ＮＶＩＤＩＡ* GPU では良好なパフォーマンスを示していても、移行後の
SYCL* コードがインテル® データセンター GPU (インテル® データセンター GPU MAX 1550 など) では、期待するパフォーマンスを示さないことがあります。その原因の 1 つとして、汎用レジスターファイル (GRF) のサイズが異なるた
め、ＮＶＩＤＩＡ* GPU でが発生しないレジスタースピルがインテル® GPU で発生することがあげられます。コードを変更することなくレジスタースピルを回避する方法については以下で説明します。この節では説明しませんが、レジスター
全体のプレッシャーを軽減する最適化テクニックについては、「レジスターの使用とパフォーマンス」を参照してください。

5-3-3-4-1

レジスター・プレッシャーの高いシンプルなコード

ここで示す例は、多変量双対数を使用して自動微分を実行するコードからヒントを得たものです。この例を理解する
ため二重数に関する知識は必要ありません。二重数の詳細については Wikipedia のページをこちらの書籍 (英語) を参照してください。
1. #include <Chrono>
2. #include <iostream>
3. #include <sycl/sycl.hpp>
4.
5. #define NREGISTERS 80
6.
7. #ifndef SIMD16
8. #define SIMD_SIZE 32
9. #else
10. #define SIMD_SIZE 16
11. #endif
12.
13. class my_type {14. public:
15. my_type(double x0, double x1, double x2, double x3, double x4, double x5,
16. double x6, double x7)
17. : x0_(x0), x1_(x1), x2_(x2), x3_(x3), x4_(x4), x5_(x5), x6_(x6), x7_(x7) {
18. }
19.
20. my_type(double const *const vals)
21. : x0_(vals[0]), x1_(vals[1]), x2_(vals[2]), x3_(vals[3]), x4_(vals[4]),22. x5_(vals[5]), x6_(vals[6]), x7_(vals[7]) {}23.
24. my_type operator+(const my_type &rhs) {25. return my_type(rhs.x0_ + x0_, rhs.x1_ + x1_, rhs.x2_ + x2_, rhs.x3_ + x3_,
26. rhs.x4_ + x4_, rhs.x5_ + x5_, rhs.x6_ + x6_, rhs.x7_ + x7_);
27. }
28.
29. void WriteBack(double *const vals) {30. vals[0] += x0_;
31. vals[1] += x1_;
32. vals[2] += x2_;
33. vals[3] += x3_;
34. vals[4] += x4_;55

oneAPI GPU 最適化ガイド
35. vals[5] += x5_;
36. vals[6] += x6_;
37. vals[7] += x7_;
38. }
39.
40. private:41. double x0_, x1_, x2_, x3_, x4_, x5_, x6_, x7_;42. };
43.
44. int main(int argc, char **argv) {45. sycl::queue Q(sycl::gpu_selector_v);
46. size_t nsubgroups;
47. if (argc > 1)
48. nsubgroups = 10;
49. else
50. nsubgroups = 8;51.
52. const size_t ARR_SIZE = nsubgroups * SIMD_SIZE * NREGISTERS;
53. double *val_in = sycl::malloc_shared(ARR_SIZE, Q);
54. double *val_out = sycl::malloc_shared(ARR_SIZE, Q);
55.
56. std::cout << "Using simd size " << SIMD_SIZE << std::endl;
57.
58. Q.parallel_for(ARR_SIZE, [=](auto it) {59. val_in[it.get_id()] = argc * it.get_id();60. }).wait();61.
62. auto start_time = std::chrono::high_resolution_clock::now();
63.
64. for (int rep = 0; rep < 1000; rep++) {65. Q.parallel_for(sycl::nd_range<1>(nsubgroups * SIMD_SIZE, SIMD_SIZE),
66. [=](auto it) [[intel::reqd_sub_group_size(SIMD_SIZE)]] {67. const size_t id = it.get_global_linear_id();68.
69. double const *const val_offs = val_in + id * NREGISTERS;
70.
71. my_type v0(val_offs + 0);
72. my_type v1(val_offs + 8);
73. my_type v2(val_offs + 16);74. my_type v3(val_offs + 24);75. my_type v4(val_offs + 32);
76. my_type v5(val_offs + 40);77. my_type v6(val_offs + 48);
78. my_type v7(val_offs + 56);
79. my_type v8(val_offs + 64);80. my_type v9(val_offs + 72);81.
82. my_type p0 = v0 + v5;
83. my_type p1 = v1 + v6;
84. my_type p2 = v2 + v7;
85. my_type p3 = v3 + v8;
86. my_type p4 = v4 + v9;87.
88. p0 = p0 + p1 + v5;
89. p2 = p2 + p3 + v6;
90. p4 = p4 + p1 + v7;
91. p3 = p3 + p2 + v8;
92. p2 = p0 + p4 + v9;
93. p1 = p1 + v0;
94.
95. double *const vals_out_offs = val_out + id * NREGISTERS;
96. p0.WriteBack(vals_out_offs);
97. p1.WriteBack(vals_out_offs + 8);
98. p2.WriteBack(vals_out_offs + 16);99. p3.WriteBack(vals_out_offs + 24);100. p4.WriteBack(vals_out_offs + 32);101. v0.WriteBack(vals_out_offs + 40);
102. v1.WriteBack(vals_out_offs + 48);103. v2.WriteBack(vals_out_offs + 56);

56

oneAPI GPU 最適化ガイド
104. v3.WriteBack(vals_out_offs + 64);
105. v4.WriteBack(vals_out_offs + 72);
106. }) .wait();107. }
108. auto end_time = std::chrono::high_resolution_clock::now();
109.
110. std::cout << "Took "111. << std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time) .count()
112. << " microseconds" << std::endl;
113. sycl::free(val_in, Q);
114. sycl::free(val_out, Q);
115. return 0;116. }
117.

このコードはクラス my_type をリストしたものです。このコードは、7 次元の双対数値と 8 つの倍精度浮動小数点のラップ、および算術オペレーターのオーバーロードを含んでいます。プログラムのメイン関数は、一部のデータを初期化し、特定の GPU カーネルを 1000 回起動します (70 行目の for ループ、72 行目のカーネル起動)。カーネルを繰り返し起動することは、より正確なパフォーマンス数値を取得することを目的としており、レジスタースピルには影響しません。カーネルは、データを使用していくつかの my_type の二重数値を初期化し、高いレジスター・プレッシャーをともなうコードで算術オペレーターを実行します。
oneAPI for NVIDIA* GPU プラグインを使用して、次のコマンドで NVIDIA* A100 GPU 向けの SYCL* コードをコン
パイルします:$ clang++ -O2 -Xcuda-ptxas -v -fsycl -fsycl-targets=nvidia_gpu_sm_80 --cudapath=/opt/hpc_software/compilers/nvidia/cuda-12.0 main.cpp -o exec_cuda.out
168 個のレジスターが割り当てられており、次の出力でスピルがないことを確認できます。
ptxas info : Used 168 registers, 380 bytes cmem[0]また、次のコマンドで oneAPI 2023.2.0 を使用して事前 (AOT) コンパイルによる PVC コードを生成します。
$ icpx -fsycl -fsycl-targets=spir64_gen
main.cpp -o exec_jit.out-Xsycl-target-backend"-device

pvc"

次のコンパイラーの警告から、レジスタースピルが発生することが分かります。
warning: kernel _ZTSZ4mainEUlT_E0_ compiled SIMD32 allocated 128 regs and spilledaround 396
次の表に、NVIDIA* A100 80GB GPU とインテル® データセンター GPU MAX 1550 (単一スタック) でのコードの実
行時間を示します。
単一スタックのインテル® マックス 1550 と NVIDIA* A100 80GB での実行時間
ハードウェア時間 (ms) 低い値が良い

レジスタースピル

A100 80GB

21.5

なし

1T マックス 1550

36.1

あり

インテル® GPU での実行時間は、NVIDIA* GPU の実行時間を大幅に上回っていることが分かります。レジスタースピ
ルの有無から、この実行時間の違いにはレジスター割り当てが関連していると考えることができます。根本的な原因を理解するため、次のセクションではレジスターファイルの違いを説明します。

57

oneAPI GPU 最適化ガイド

5-3-3-4-2

インテル® データセンター GPU MAX 1550 のレジスター

レジスター割り当ての違いは、汎用レジスターファイル (GRF) のサイズの違いによるものです。NVIDIA＊ A100 GPU
は、work-item ごとに最大 256 個の 32 ビット・レジスターを提供しますが、インテル® データセンター GPU MAX
1550 は、デフォルトで 64 個の 32 ビット・レジスターを提供します (sub-group ごとに同数の 32 work-item を想定。インテル® Xe GPU アーキテクチャーを参照)。これは、 NVIDIA* GPU の 4 分の 1 です。利用可能なレジスター数を調整するには 2 つの方法があります。それは、ラージ・レジスター・モード (スモール・レジスター・モードとラージ・レ
ジスター・モードを参照) と sub-group サイズの縮小 (sub-group と SIMD ベクトル化を参照) です。上記の例に対す
るこのオプションの影響については、次のセクションで説明します。
インテル® GPU MAX 1550 でのラージ GRF ありなしと SIMD 幅の違い
ハードウェアSIMD 幅GRF モード
A100 80GB
32
なし
1T マックス 1550
32
デフォルト
1T マックス 1550
32
ラージ GRF
1T マックス 1550
16
デフォルト
1T マックス 1550
16
ラージ GRF

5-3-3-4-3

Work-item ごとのレジスター
256 x 32 ビット
64 x 32 ビット
１２８ x 32 ビット
128 x 32 ビット
256 x 32 ビット

Work-item ごとのレジスター数を増加

前述の例では 168 個のレジスターが必要です。これまでの説明から、ラージ GRF モードと 16 個の縮小された SIMD
幅を組み合わせることで、レジスタースピルの問題は回避できるはずです。以下では、GRF モードと SIMD 幅の組み合
わせを確認します。実際にすべてのレジスタースピルを回避するには、ラージ GRF モードと SIMD 16 が必要であることが分かります。
次のコマンドでコードをラージ GRF モードでコンパイルします。
$ icpx -fsycl -fsycl-targets=spir64_gen -Xsycl-target-backend "-device pvc -options-ze-opt-largeregister-file" main.cpp -o exec_largegrf.out
以前より減少しましたが、レジスタースピルに関する警告はまだ表示されます。
warning: kernel _ZTSZ4mainEUlT_E0_ compiled SIMD32 allocated 256 regs and spilledaround 206
これは、１６８ 個のレジスターが必要であるのに 128 個の 32 ビット・レジスターしか利用できないことが想定されます。
レジスタースピルを減らすため 2 番目のオプションを検討します。これは、コードに最小限の変更を加えて SIMD 幅を32 から 16 に減らすことです。これには、次のコマンドを使用します。
$ icpx -DSIMD16 -fsycl -fsycl-targets=spir64_gen -Xsycl-target-backend "-devicepvc" main.cpp -o exec_simd16.out
SIMD 幅 16 でデフォルトの GRF モードを使用すると、レジスタースピルに関する警告が再び表示されます。
warning: kernel _ZTSZ4mainEUlT_E0_ compiled SIMD16 allocated 128 regs and spilledaround 112
SIMD 幅 16 への縮小とラージ GRF モードの両方を使用すると、すべてのレジスタースピルが排除され、コンパイラー
は警告を発生しなくなります。これは、最初に期待したものです。
58

oneAPI GPU 最適化ガイド
次の表に 4 つの構成の実行時間を示します。
単一スタックインテル® データセンター GPU MAX 1550 の異なる設定での実行時間
GRF モード

SIMD 幅レジスタースピル

時間 (ms) 低いほど良い

デフォルト

32

あり

36.1

ラージ GRF

32

あり

32.2

デフォルト

16

あり

30.3

ラージ GRF

16

なし

27.7

明らかに、レジスタースピルが発生しない場合にパフォーマンスが向上することが分かります。興味深いことは、SIMD 幅 16 がラージ GRF モードよりも好ましいということです。ラージ GRF モードでは、ハードウェア・スレッドの半分が無効になることを考えると、それほど驚くべきことではありません。

5-3-3-4-4

SIMD 16 へポーティングする際の考慮事項

上記の方法が適さない、または特別な注意が必要なことがあります。一方、ラージ GRF モードを使用すると、ハード
ウェア・スレッドの半分が無効になり、特定のコードのパフォーマンスが低下する可能性があります。
SIMD ３２ から SIMD 16 への移行により、誤った結果が生じる可能性があるため、注意を要する場合があります。次にコード例を示します。
1. #include <chrono>
2. #include <iostream>
3. #include <Sycl/sycl.hpp>
4.
5. #define NREGISTERS 80
6.
7. #ifndef SIMD16
8. #define SIMD_SIZE 32
9. #else
10. #define SIMD_SIZE 16
11. #endif
12.
13. class my_type {14. public:
15. my_type(double x0, double x1, double x2, double x3, double x4, double x5,
16. double x6, double x7)
17. : x0_(x0), x1_(x1), x2_(x2), x3_(x3), x4_(x4), x5_(x5), x6_(x6), x7_(x7) {
18. }
19.
20. my_type(double const *const vals)
21. : x0_(vals[0]), x1_(vals[1]), x2_(vals[2]), x3_(vals[3]), x4_(vals[4]),22.
x5_(vals[5]), x6_(vals[6]), x7_(vals[7]) {}23.
24. my_type operator+(const my_type &rhs) {25. return my_type(rhs.x0_ + x0_, rhs.x1_ + x1_, rhs.x2_ + x2_, rhs.x3_ + x3_,
26. rhs.x4_ + x4_, rhs.x5_ + x5_, rhs.x6_ + x6_, rhs.x7_ + x7_);
27. }
28.
29. void WriteBack(double *const vals) {30. vals[0] += x0_;
31. vals[1] += x1_;
32. vals[2] += x2_;
33. vals[3] += x3_;
34. vals[4] += x4_;
35. vals[5] += x5_;
36. vals[6] += x6_;
37. vals[7] += x7_;
38. }

59

oneAPI GPU 最適化ガイド
39.
40. void Load(double const *const vals) {41. x0_ = vals[0];42. x1_ = vals[1];43. x2_ = vals[2];44. x3_ = vals[3];45. x4_ = vals[4];46. x5_ = vals[5];47. x6_ = vals[6];48. x7_ = vals[7];
49. }
50.
51. private:52. double x0_, x1_, x2_, x3_, x4_, x5_, x6_, x7_;53. };
54.
55. int main(int argc, char **argv) {56. sycl::queue Q(sycl::gpu_selector_v);
57. size_t nsubgroups;
58. if (argc > 1)
59. nsubgroups = 10;
60. else
61. nsubgroups = 8;62.
63. const size_t ARR_SIZE = nsubgroups * SIMD_SIZE * NREGISTERS;
64. double *val_in = sycl::malloc_shared(ARR_SIZE, Q);
65. double *val_out = sycl::malloc_shared(ARR_SIZE, Q);
66.
67. std::cout << "Using simd size " << SIMD_SIZE << std::endl;
68.
69. Q.parallel_for(ARR_SIZE, [=](auto it) {70. val_in[it.get_id()] = argc * it.get_id();71. }).wait();72.
73. auto start_time = std::chrono::high_resolution_clock::now();
74.
75. for (int rep = 0; rep < 1000; rep++) {76. Q.submit([&](auto &h) {77. sycl::accessor <double, 1, sycl::access::mode::read_write,
78. sycl;;access::targetlocal>
79. slm(sycl::range(32 * 8), h);80.
81. h.parallel_for(sycl::nd_range(nsubgroups * SIMD_SIZE, 32),
82. [=](auto it) [[intel::reqd_sub_group_size(SIMD_SIZE)]] {83. const int id = it.get_global_linear_id();
84. const int local_id = it.get_local_id(0);85. for (int i = 0; i < 8; i++) {86. slm[i + local_id * 8] = 0.0;
87. }88. #ifdef SIMD16
89. it.barrier(sycl::access::fence_space::local_space);
90. #endif
91. double const *const val_offs = val_in + id * NREGISTERS;
92. my_type v0(val_offs + 0);
93. my_type v1(val_offs + 8);
94. my_type v2(val_offs + 16);
95. my_type v3(val_offs + 24);
96. my_type v4(val_offs + 32);
97. my_type v5(val_offs + 40);98. my_type v6(val_offs + 48);
99. my_type v7(val_offs + 56);
100. my_type v8(val_offs + 64);
101. my_type v9(val_offs + 72);102.
103. my_type p0 = v0 + v5;
104. v0.WriteBack(&slm[local_id * 8]);105. #ifdef SIMD16
106. it.barrier(sycl::access::fence_space::local_space);107. #endif

60

oneAPI GPU 最適化ガイド
108. v0.Load(&slm[32 * 8 - 8 - local_id * 8]);109. my_type p1 = v1 + v6;
110. my_type p2 = v2 + v7;
111. my_type p3 = v3 + v8;
112. my_type p4 = v4 + v9;113.
114. p0 = p0 + p1 + v5;
115. p2 = p2 + p3 + v6;
116. p4 = p4 + p1 + v7;
117. p3 = p3 + p2 + v8;
118. p2 = p0 + p4 + v9;
119. p1 = p1 + v0;120.
121. double *const vals_out_offs = val_out + id * NREGISTERS;
122. p0.WriteBack(vals_out_offs);
123. p1.WriteBack(vals_out_offs + 8);
124. p2.WriteBack(vals_out_offs + 16);125. p3.WriteBack(vals_out_offs + 24);
126. p4.WriteBack(vals_out_offs + 32);
127. v0.WriteBack(vals_out_offs + 40);
128. v1.WriteBack(vals_out_offs + 48);129. v2.WriteBack(vals_out_offs + 56);130. v3.WriteBack(vals_out_offs + 64);131. v4.WriteBack(vals_out_offs + 72);132. });
133. }).wait();134. }
135.
136. auto end_time = std::chrono::high_resolution_clock::now();
137.
138. std::cout << "Took "139. << std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time) .count()
140. << " microseconds" << std::endl;
141.
142. sycl::free(val_in, Q);
143. sycl::free(val_out, Q);
144. return 0;145. }

これは、最初の例に似ていますが、各 work-group (SIMD を使用する場合は sub-group) 内の v0 に格納されている値を転置する際に共有ローカル・メモリー・アレイ (SLM – Shared Local Memory) が導入されています。
次の図は、2 番目のコード例で SLM がどのように使用されるかを示しています。Work-group 内の各 work-itemの v0 値は SLM に保存されます。v０ は 8 つの double 値 x0 – x7 で構成されています。

61

oneAPI GPU 最適化ガイド
ｖ０ 値を SLM に保存した後、逆の順番で読み取ります。
ＳＬＭ に保存される work-item のローカル id

ＳＬＭ からロードされる work-item のローカル id

0

31

1

30

2

29

…

…

29

2

30

1

31

0

この場合、SIMD 32 から SIMD 16 への切り替えでは、一般に誤った結果を生じます。それは、SIMD 16 の場合、SLMにアクセスする 32 個の work-item 間の同期が保証されないためです。SIMD 32 では、work-item は同じ subgroup にあり、一緒に処理されるためこれは問題になりません (sub-group と SIMD ベクトル化を参照)。つまり、
SIMD 16 では、SLM 値が実際に書き込まれる (116 行目) 前に読み取られて使用される (120 行目) 問題が発生する可能性があります。この問題を解決するには、118 行目に示すように SLM バリアを追加して、値が読み取られる前にすべての値が書く込まれることを保証する必要があります。ただし、これらのバリアは計算時間に影響する可能性があることを忘れないでください。次の表に、4 つの構成における計算時間を示します。
各種設定による単一スタックのインテル® データセンター GPU MAX 1550 での SLM サンプルの実行時間。
GRF モード

SIMD 幅デフォルト
ラージ GRF
デフォルト
ラージ GRF

32
32
16
16

レジスタースピル
あり
あり
あり
なし

時間 (ms) 低いほど良い
38.0
33.7
31.3
29.7

ここまでをまとめると、高いレジスター・プレッシャーがあるコードを NVIDIA* GPU からインテル® データセンター
GPU MAX 1550 に移植する場合、異なる GRF サイズによってレジスタースピルが発生し、コードは期待するパフォー
マンスを示さない場合があります。このような場合、最小限のコード調整で GRF サイズを増やし、各種デバイスのパフォーマンスを最適化する方法があります。

5-3-4

共有ローカルメモリー

ほとんどのケースでは、work-item はデータを共有して相互に通信を行う必要があります。一方、すべての workgroup 内のすべての work-item はグローバルメモリーにアクセスできることから、データ共有と通信はグローバル
メモリー経由で行うことができます。ただし、グローバルメモリーの帯域幅は低く、レイテンシーが長いため共有と通信の効率は低下します。ベクトルエンジン (ＶＥ) スレッドが同時に実行する sub-group 内の work-item は、データを共有して互いに効率良く通信できますが、sub-group 内の work-item 数は少ないため共有と通信の範囲は限定されます。広範囲の work-item にアクセスできる高い帯域幅の低レイテンシーのメモリーは、work-item 間のデータ共有と通信には理想的です。インテル® GPU に搭載される共有ローカルメモリー (SLM) は、そのような用途向けに設計されています。
インテル® GPU の各 Xe コアには個別の SLM があります。SLM へのアクセスは、Xe コアの VE、または同じ Xe コアのVE で実行されるようにスケジュールされた同じ work-group 内の work-item に限定されます。これは Xe コア (または work-group) に対してローカルであり、同じ Xe コア (または同じ work-group 内の work-item) 内の VE によって共有されるため SLM と呼ばれます。SLM は各 Xe コアでオンチップであるため、グローバルメモリーよりもはるかに高い帯域幅と低いレイテンシーを持っています。また、SLM は work-group 内のすべての work-item にアクセスできることから、work-group サイズに応じて数百の work-item 間のデータ共有と通信に対応できます。
62

oneAPI GPU 最適化ガイド
SLM は work-group が管理するキャッシュであると考えることができます。work-group が開始されると、workgroup 内の work-item はグローバルメモリーから SLM にデータを明示的にロードでき、データを高速にアクセスするため work-group のライフタイム中 SLM に保持できます。work-group が終了する前に、明示的に work-item に
よって SLM 内のデータをグローバルメモリーに書き戻すことができます。work-group の実行が完了すると、SLM のデータも失われ無効化されます。SLM とグローバルメモリー間の整合性を保つのはプログラマーの責任です。適切にSLM を使用することでパフォーマンスを大幅に向上できます。

5-3-4-1

共有ローカルメモリーのサイズと work-group サイズ

オンチップの SLM のサイズは制限されます。work-group で利用可能なメモリー量はデバイスによって異なりますが、デバイスに照会することで確認できます。以下に例を示します。
std::cout << "Local Memory Size: "<< q.get_device().get_info<sycl::info::device::local_mem_size>()<< std::endl;

出力は以下のようになります。
Local Memory Size: 65536

サイズはバイト単位です。この GPU デバイスには、work-group ごとに 64,536 バイト (64KB) の SLM があることが分かります。
work-group が利用できる最大 SLM サイズを知ることが重要です。多くの場合、work-group で利用可能な SLM の合計サイズは、work-group 内の work-item 数で一定ではない関数です。SLM の最大数は、グループ内の workitem の総数、つまり work-group サイズを制限することがあります。例えば、最大 SLM サイズが 64KB で、それぞれの work-item が 512 バイトの SLM を必要とする場合、最大 work-group サイズが 128 を超えることはありません。

5-3-4-2

バンク競合

SLM は、高帯域幅で同時アクセス可能な同じサイズのメモリーバンクに分割されます。バンクの合計数はデバイスによって異なります。この例では、連続する 64 バイトが 4 バイト (32 ビット) の粒度で 16 個のバンクに格納されます。
異なるバンクへのアクセス要求は並行して処理できますが、同一バンクの異なるアドレス要求はバンク競合を発生させシリアル化されます。バンク競合はパフォーマンスに悪影響を及ぼします。次の例について考えてみます。
1. constexpr int N = 32;
2. int *data = sycl::malloc_shared<int>(N, q);
3. auto e = q.submit([&](auto &h) {4.
sycl::local_accessor<int, 1> slm(sycl::range(32 * 64), h);5.
h.parallel_for(sycl::nd_range(sycl::range{N}, sycl::range{32}),6.
[=](sycl::nd_item<1> it) {7.
int i = it.get_global_linear_id();8.
int j = it.get_local_linear_id();9.
slm[j * 16] = 0;
10.
it.barrier(sycl::access::fence_space::local_space);11.
for (int m = 0; m < 1024 * 1024; m++) {12.
slm[j * 16] += i * m;13.
it.barrier(sycl::access::fence_space::local_space);
14.
}
15.
data[i] = slm[j * 16];
16.
});17. });

バンク数が 16 である場合、上記のすべての work-item は、同じバンクの異なるアドレスの読み取りと書き込みを行います。それぞれのバンクのメモリー帯域幅は全体の 1/16 になります。
63

oneAPI GPU 最適化ガイド
次の例では、SLM バンクの競合がなく、すべての work-item が異なるバンクの異なるアドレスを読み書きするためメモリー帯域幅を完全に活用します。
1. constexpr int N = 32;
2. int *data = sycl::malloc_shared<int>(N, q);
3. auto e = q.submit([&](auto &h) {4.
sycl::local_accessor<int, 1> slm(sycl::range(32 * 64), h);5.
h.parallel_for(sycl::nd_range(sycl::range{N}, sycl::range{32}),6.
[=](sycl::nd_item<1> it) {7.
int i = it.get_global_linear_id();8.
int j = it.get_local_linear_id();9.
slm[j] = 0;
10.
it.barrier(sycl::access::fence_space::local_space);11.
for (int m = 0; m < 1024 * 1024; m++) {12.
slm[j] += i * m;13.
it.barrier(sycl::access::fence_space::local_space);
14.
}
15.
data[i] = slm[j];16.
});17. });

5-3-4-3

データ共有と work-group バリア「レジスターとパフォーマンス」の項の 256 値域のヒストグラムの例を見直してみましょう。
1. constexpr int BLOCK_SIZE = 256;
2. constexpr int NUM_BINS = 256;3.
4. std::vector<unsigned long> hist(NUM_BINS, 0);5.
6. sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
7. sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);8.
9. auto e = q.submit([&](auto &h) {10.
sycl::accessor macc(mbuf, h, sycl::read_only);
11.
auto hacc = hbuf.get_access<sycl::access::mode::atomic>(h);12.
h.parallel_for(13.
sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),14.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {15.
int group = it.get_group()[0];16.
int gSize = it.get_local_range()[0];17.
auto sg = it.get_sub_group();18.
int sgSize = sg.get_local_range()[0];19.
int sgGroup = sg.get_group_id()[0];20.
21.
unsigned int histogram[NUM_BINS / 16]; // 分布図はレジスターに格納するには
22.
// ストレージを消費しすぎます23.
for (int k = 0; k < NUM_BINS / 16; k++) {24.
histogram[k] = 0;
25.
}
26.
for (int k = 0; k < BLOCK_SIZE; k++) {27.
unsigned long x =28.
sg.load(macc.get_pointer() + group * gSize * BLOCK_SIZE +29.
sgGroup * sgSize * BLOCK_SIZE + sgSize * k);
30. // subgroup サイズは 16
31. #pragma unroll
32.
for (int j = 0; j < 16; j++) {33.
unsigned long y = sycl::group_broadcast(sg, x, j);
34. #pragma unroll
35.
for (int i = 0; i < 8; i++) {36.
unsigned int c = y & 0xFF;
37.
// (c & 0xF) は値域が存在する work-item です。
38.

64// (c >> 4) は値域のインデックスです。

oneAPI GPU 最適化ガイド
39.
if (sg.get_local_id()[0] == (c & 0xF)) {40.
histogram[c >> 4] += 1;
41.
}
42.
y = y >> 8;
43.
}
44.
}
45.
}
46.
47.
for (int k = 0; k < NUM_BINS / 16; k++) {48.
hacc[16 * k + sg.get_local_id()[0]].fetch_add(histogram[k]);
49.
}
50.
});
51. });

この例を最適化して、long の代わりに int データタイプを使用し、最適なパフォーマンスを得るためプライベート・ヒ
ストグラムの値域がレジスターに収まるようにして sub-group 内のレジスターを共有します。より多くの値域サイズ(例えば 1024) が必要な場合、プライベート・ヒストグラムの値域がグローバルメモリーにあふれることは避けられません。
ヒストグラムの値域は、それぞれの値域がアトミックに更新される限り、work-group 内の work-item 間で共有できます。
1. constexpr int NUM_BINS = 1024;
2. constexpr int BLOCK_SIZE = 256;3.
4. std::vector<unsigned long> hist(NUM_BINS, 0);
5. sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
6. sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);7.
8. auto e = q.submit([&](auto &h) {9.
sycl::accessor macc(mbuf, h, sycl::read_only);10.
sycl::accessor hacc(hbuf, h, sycl::read_write);11.
sycl::local_accessor<unsigned int, 1> local_histogram(sycl::range(NUM_BINS), h);12.
h.parallel_for(13.
sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),14.
[=](sycl::nd_item<1> it) {15.
int group = it.get_group()[0];16.
int gSize = it.get_local_range()[0];17.
auto sg = it.get_sub_group();18.
int sgSize = sg.get_local_range()[0];19.
int sgGroup = sg.get_group_id()[0];20.
21.
int factor = NUM_BINS / gSize;
22.
int local_id = it.get_local_id()[0];23.
if ((factor <= 1) && (local_id < NUM_BINS)) {24.
sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,
25.
sycl::memory_scope::device,26.
sycl::access::address_space::local_space>27.
local_bin(local_histogram[local_id]);28.
local_bin.store(0);29.
} else {30.
for (int k = 0; k < factor; k++) {31.
sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,
32.
sycl::memory_scope::device,33.
sycl::access::address_space::local_space>34.
local_bin(local_histogram[gSize * k + local_id]);35.
local_bin.store(0);
36.
}
37.
}
38.
39.
it.barrier(sycl::access::fence_space::local_space);40.
41.
for (int k = 0; k < BLOCK_SIZE; k++) {42.
unsigned long x =43.
sg.load(macc.get_pointer() + group * gSize * BLOCK_SIZE +44.
sgGroup * sgSize * BLOCK_SIZE + sgSize * k);65

oneAPI GPU 最適化ガイド
45. #pragma unroll
46.
for (std::uint8_t shift : {0, 16, 32, 48}) {47.
constexpr unsigned long mask = 0x3FFU;
48.
sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,49.
sycl::memory_scope::device,50.
sycl::access::address_space::local_space>51.
local_bin(local_histogram[(x >> shift) & mask]);52.
local_bin += 1;
53.
}
54.
}
55.
it.barrier(sycl::access::fence_space::local_space);56.
57.
if ((factor <= 1) && (local_id < NUM_BINS)) {58.
sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,
59.
sycl::memory_scope::device,60.
sycl::access::address_space::local_space>61.
local_bin(local_histogram[local_id]);62.
sycl::atomic_ref<unsigned long, sycl::memory_order::relaxed,
63.
sycl::memory_scope::device,64.
sycl::access::address_space::global_space>65.
global_bin(hacc[local_id]);66.
global_bin += local_bin.load();67.
} else {68.
for (int k = 0; k < factor; k++) {69.
sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,
70.
sycl::memory_scope::device,71.
sycl::access::address_space::local_space>72.
local_bin(local_histogram[gSize * k + local_id]);73.
sycl::atomic_ref<unsigned long, sycl::memory_order::relaxed,74.
sycl::memory_scope::device,75.
sycl::access::address_space::global_space>76.
global_bin(hacc[gSize * k + local_id]);77.
global_bin += local_bin.load();
78.
}
79.
}
80.
});
81. });

work-group が開始されると、work-group 内の各 work-item は SLM の histogram の値域を 0 に初期化します(上記コードの 20 - 34 行目)。単一の work-item ですべての histogram の値域を初期化することもできますが、通常は、work-group 内のすべての work-item でジョブを分散するほうが効果的です。
35 行目にある初期化後の work-group バリアは、work-item が値域を更新する前にすべての histogram 値域が0 に初期化されることを保証します。
SLM にある histogram 値域はすべての work-item で共有されるため、任意の work-item による値域の更新はア
トミックでなければなりません。
work-group 内のローカル分布図が完成すると、グローバル histogram が更新されます。ただし、ローカル SLM の値域を読み取ってグローバル値域を更新する前に、50 行目で work-group バリアを再度呼び出してすべての workitem のワークが完了するのを待機します。
SLM のデータを共有する場合、work-item の同期に work-group バリアが必要になることがあります。バリアにはコストが生じるため、work-group のサイズが増えるとコストも増加します。アプリケーションに最適な work-group サ
イズを特定するため、いろいろなサイズを試してください。
また、examples フォルダーには 256 個の値域を持つ SLM バージョンのヒストグラムも用意されています。レジス
ターを使用したバージョンとパフォーマンスを比較することもできます。予想を上回る結果を得られても、さらにパフォーマンスを向上する最適化が可能であるか考えることもあるでしょう。これは演習として皆さんにお任せします。

66

oneAPI GPU 最適化ガイド

5-3-4-4

キャッシュとして SLM を使用

ハードウェアにキャッシュの管理を任せるよりも、アプリケーションでホットなデータの管理を行うほうが望ましい場
合があります。アプリケーションでデータのキャッシングを直接管理すると、データが必要なときにそのデータがどこにあるのか、そしてデータアクセスのコストを正確に推測できます。SLM はそのような用途に利用できます。
次の 1-D 畳み込みの例について考えてみます。
1.

sycl::buffer<int> ibuf(input.data(), N);
2. sycl::buffer<int> obuf(output.data(), N);
3. sycl::buffer<int> kbuf(kernel.data(), M);
4.
5. auto e = q.submit([&](auto &h) {6. sycl::accessor iacc(ibuf, h, sycl::read_only);
7. sycl::accessor oacc(obuf, h);
8. sycl::accessor kacc(kbuf, h, sycl::read_only);
9.
10. h.parallel_for(sycl::nd_range<1>(sycl::range{N}, sycl::range{256}),
11.
[=](sycl::nd_item<1> it) {12.
int i = it.get_global_linear_id();13.
int group = it.get_group()[0];14.
int gSize = it.get_local_range()[0];15.
16.
int t = 0;
17.
int _M = static_cast<int>(M);18.
int _N = static_cast<int>(N);19.
20.
if ((group == 0) || (group == _N / gSize - 1)) {21.
if (i < _M / 2) {22.
for (int j = _M / 2 - i, k = 0; j < _M; ++j, ++k) {23.
t += iacc[k] * kacc[j];
24.
}
25.
} else {26.
if (i + _M / 2 >= _N) {27.
for (int j = 0, k = i - _M / 2;j < _M / 2 + _N - i; ++j, ++k) {28.
t += iacc[k] * kacc[j];
29.
}
30.
} else {31.
for (int j = 0, k = i - _M / 2; j < _M; ++j, ++k) {32.
t += iacc[k] * kacc[j];
33.
}
34.
}
35.
}
36.
} else {37.
for (int j = 0, k = i - _M / 2; j < _M; ++j, ++k) {38.
t += iacc[k] * kacc[j];
39.
}
40.
}
41.
42.
oacc[i] = t;
43.
});44. });

この例では、257 要素のカーネル配列を使用して、8192 x 8192 要素の整数配列を畳み込んで結果を出力配列に書き込みます。各 work-item は 1 つの要素を畳み込みます。ただし、1 つの要素を畳み込むには、最大 256 個の隣接する要素が必要になります。
各入力要素が複数の work-item で使用されていることを知っていれば、work-group 全体で必要な要素を SLM に事
前ロードすることができ、以降で要素が必要になった際に、グローバルメモリーに代わって SLM からロードできます。

67

oneAPI GPU 最適化ガイド
1. sycl::buffer<int> ibuf(input.data(), N);
2. sycl::buffer<int> obuf(output.data(), N);
3. sycl::buffer<int> kbuf(kernel.data(), M);
4.
5. auto e = q.submit([&](auto &h) {6.
sycl::accessor iacc(ibuf, h, sycl::read_only);7.
sycl::accessor oacc(obuf, h);8.
sycl::accessor kacc(kbuf, h, sycl::read_only);9.
sycl::local_accessor<int, 1> ciacc(sycl::range(256 + (M / 2) * 2), h);10.
11.
h.parallel_for(12.
sycl::nd_range(sycl::range{N}, sycl::range{256}),13.
[=](sycl::nd_item<1> it) {14.
int i = it.get_global_linear_id();15.
int group = it.get_group()[0];16.
int gSize = it.get_local_range()[0];17.
int local_id = it.get_local_id()[0];18.
int _M = static_cast<int>(M);19.
20.
ciacc[local_id + M / 2] = iacc[i];21.
22.
if (local_id == 0) {23.
if (group == 0) {24.
for (int j = 0; j < _M / 2; ++j) {25.
ciacc[j] = 0;
26.
}
27.
} else {28.
for (int j = 0, k = i - _M / 2; j < _M / 2; ++j, ++k) {29.
ciacc[j] = iacc[k];
30.
}
31.
}
32.
}
33.
if (local_id == gSize - 1) {34.
if (group == static_cast<int>(it.get_group_range()[0]) - 1) {35.
for (int j = gSize + _M / 2; j < gSize + _M / 2 + _M / 2; ++j) {36.
ciacc[j] = 0;
37.
}
38.
} else {39.
for (int j = gSize + _M / 2, k = i + 1;j < gSize+_M/2+_M /2; ++j, ++k) {40.
ciacc[j] = iacc[k];
41.
}
42.
}
43.
}
44.
45.
it.barrier(sycl::access::fence_space::local_space);
46.
47.
int t = 0;48.
for (int j = 0, k = local_id; j < _M; ++j, ++k) {49.
t += ciacc[k] * kacc[j];
50.
}
51.
52.
oacc[i] = t;
53.
});54. });

work-group が開始されると、各 work-item で必要とされるすべての入力要素は SLM にロードされます。最初と最後の work-item を除いて、各 work-item は 1 つの要素を SLM にロードします。最初の work-item は最初の要素の左に隣接する要素をロードし、最後の work-item は SLM の最後の要素の右に隣接する要素をロードします。隣接する要素がない場合、SLM の要素は 0 で埋められます。
各 work-item で畳み込みを開始する前に、ローカルバリアを呼び出し、すべての入力要素が SLM にロードされるのを待機します。
各 work-item の畳み込みはシンプルです。隣接するすべての要素は、グローバルメモリーではなく高速な SLM からロードされます。
68

oneAPI GPU 最適化ガイド

5-3-4-5

SLM エラーのトラブルシューティング

PI_ERROR_OUT_OF_RESOURCES エラーは、カーネルがハードウェアで使用可能な量よりも多くの共有ローカル
メモリーを使用すると発生する可能性があります。このエラーが発生すると、次のようなエラーメッセージが表示されます。
$ ./myapp:terminate called after throwing an instance of 'sycl::_V1::runtime_error'
what(): Native API failed. Native API returns: -5(PI_ERROR_OUT_OF_RESOURCES) -5 (PI_ERROR_OUT_OF_RESOURCES)
Aborted (core dumped)$

共有ローカルメモリーにコピーされたメモリーとハードウェアの上限を確認するには、デバッグキーを設定します。
export PrintDebugMessages=1
export NEOReadDebugKeys=1

これにより、出力は次のようになります。
$ ./myapp:Size of SLM (656384) larger than available (131072)
terminate called after throwing an instance of 'cl::sycl::runtime_error' what():
Native API failed.Native API returns: -5 (CL_OUT_OF_RESOURCES) -5(CL_OUT_OF_RESOURCES)
Aborted (core dumped)
$

5-3-5

ポインターのエイリアシングと restrict ディレクティブ

カーネルは通常、ポインター引数として渡される要素の配列を操作します。コンパイラーがポインターの相互エイリ
アスを判断できない場合、安全性の観点から保守的にエイリアスがあると想定し、コンパイラーはポインターに対する操作のリオーダーを行いません。ループ反復内に 2 つのロードと 1 つのストアがあるベクトル加算の例を考えてみます。
1. size_t VectorAdd(sycl::queue &q, const IntArray &a, const IntArray &b,
2.
IntArray &sum, int iter) {3.
sycl::range num_items{a.size()};4.
5.
sycl::buffer a_buf(a);6.
sycl::buffer b_buf(b);7.
sycl::buffer sum_buf(sum.data(), num_items);
8.
9.
auto start = std::chrono::steady_clock::now();10.
for (int i = 0; i < iter; i++) {11.
auto e = q.submit([&](auto &h) {12.
// 入力アクセサー
13.
sycl::accessor a_acc(a_buf, h, sycl::read_only);14.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
15.
// 出力アクセサー
16.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
17.
h.parallel_for(num_items,
18.
[=](auto i) { sum_acc[i] = a_acc[i] + b_acc[i]; });
19.
});
20.
}
21.
q.wait();22.
auto end = std::chrono::steady_clock::now();23.
std::cout << "Vector add completed on device - took " << (end - start).count()69

oneAPI GPU 最適化ガイド
24.
<< " u-secs\n";25.
return ((end - start).count());
26. }// VectorAdd の終わり

ここでは、プログラマーはベクトル長と work-group 数の選択をコンパイラーに任せています。ほとんどのケースでは、コンパイラーは最高のパフォーマンスを得るため適切なパラメーターを選択することができます。しかし、パフォーマンス向上のため work-group 数と work-group サイズを明示的に指定して、コンパイラーにヒントを与える
ことが適切である場合があります。
次のカーネルは、work-item ごとに配列の複数の要素を処理するように記述されており、work-group 数と workgroup サイズを明示的に指定しています。23 行目の intel::kernel_args_restrict は、このカーネルのバッファーアクセサーが互いにエイリアスを持たないことをコンパイラーに通知します。これにより、コンパイラーは
ロードとストアのホイストが可能となり、命令が完了するまでの時間が長くなることから命令のスケジュールが改善されます。26 行目のプラグマは、2 回のアンロールを行うことをコンパイラーに指示します。
1. size_t VectorAdd2(sycl::queue &q, const IntArray &a, const IntArray &b,
2.
IntArray &sum, int iter) {3.
sycl::range num_items{a.size()};4.
sycl::buffer a_buf(a);5.
sycl::buffer b_buf(b);6.
sycl::buffer sum_buf(sum.data(), num_items);
7. // size_t num_groups =8. // q.get_device().get_info<sycl::info::device::max_compute_units>(); size_t
9. // wg_size =10. // q.get_device().get_info<sycl::info::device::max_work_group_size>();11.
size_t num_groups = 1;
12.
size_t wg_size = 16;
13.
auto start = std::chrono::steady_clock::now();14.
for (int i = 0; i < iter; i++) {15.
q.submit([&](auto &h) {16.
// 入力アクセサー
17.
sycl::accessor a_acc(a_buf, h, sycl::read_only);18.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
19.
// 出力アクセサー
20.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
21.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),22.
[=](sycl::nd_item<1> index) [[intel::reqd_sub_group_size(16)]]23.
[[intel::kernel_args_restrict]] {24.
size_t loc_id = index.get_local_id();25.
// unroll with a directive
26. #pragma unroll(2)27.
for (size_t i = loc_id; i < mysize; i += wg_size) {28.
sum_acc[i] = a_acc[i] + b_acc[i];
29.
}
30.
});
31.
});
32.
}
33.
q.wait();34.
auto end = std::chrono::steady_clock::now();35.
std::cout << "Vector add2 completed on device - took "36.
<< (end - start).count() << " u-secs\n";37.
return ((end - start).count());
38. } // VectorAdd2 の終わり

次のカーネルは、コンパイラー・ディレクティブの代わりに、ループを手動でアンロールする方法を示します (コンパイ
ラーは、内部のヒューリスティックによるコストモデルによって、ディレクティブを受け入れる場合とそうでない場合があります)。アンロールの利点は、ループを繰り返す回数が減るため、実行される命令数が少なくなることです (比較命
令と分岐命令の数が減ります)。

70

oneAPI GPU 最適化ガイド
1. size_t VectorAdd3(sycl::queue &q, const IntArray &a, const IntArray &b,
2.
IntArray &sum, int iter) {3.
sycl::range num_items{a.size()};4.
sycl::buffer a_buf(a);5.
sycl::buffer b_buf(b);6.
sycl::buffer sum_buf(sum.data(), num_items);
7.
size_t num_groups = 1;8.
size_t wg_size = 16;9.
auto start = std::chrono::steady_clock::now();10.
for (int i = 0; i < iter; i++) {11.
q.submit([&](auto &h) {12.
// 入力アクセサー
13.
sycl::accessor a_acc(a_buf, h, sycl::read_only);14.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
15.
// 出力アクセサー
16.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
17.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),18.
[=](sycl::nd_item<1> index) [[intel::reqd_sub_group_size(16)]] {19.
// 手動でアンロール
20.
size_t loc_id = index.get_local_id();21.
for (size_t i = loc_id; i < mysize; i += 32) {22.
sum_acc[i] = a_acc[i] + b_acc[i];23.
sum_acc[i + 16] = a_acc[i + 16] + b_acc[i + 16];
24.
}
25.
});
26.
});
27.
}
28.
q.wait();29.
auto end = std::chrono::steady_clock::now();30.
std::cout << "Vector add3 completed on device - took "31.
<< (end - start).count() << " u-secs\n";32.
return ((end - start).count());
33. } // VectorAdd3 の終わり

次のカーネルは、ロードとストアを再配置することで、データを必要とする操作が実行される前にロードが発行されるようにする方法を示しています。通常、GPU 上のすべてのスレッドには多くの未処理のロードが存在する可能性があります。ロードするデータを処理する前にロードを発行することを推奨します。これにより、データが計算で実際に使用される前にロードを完了することが可能になります。
1. size_t VectorAdd4(sycl::queue &q, const IntArray &a, const IntArray &b,
2.
IntArray &sum, int iter) {3.
sycl::range num_items{a.size()};4.
sycl::buffer a_buf(a);5.
sycl::buffer b_buf(b);6.
sycl::buffer sum_buf(sum.data(), num_items);
7.
size_t num_groups = 1;8.
size_t wg_size = 16;9.
auto start = std::chrono::steady_clock::now();10.
for (int i = 0; i < iter; i++) {11.
q.submit([&](auto &h) {12.
// 入力アクセサー
13.
sycl::accessor a_acc(a_buf, h, sycl::read_only);14.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
15.
// 出力アクセサー
16.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
17.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),18.
[=](sycl::nd_item<1> index)[[intel::reqd_sub_group_size(16)]] {19.
// 手動アンロール
20.
size_t loc_id = index.get_local_id();21.
for (size_t i = loc_id; i < mysize; i += 32) {22.
int t1 = a_acc[i];23.
int t2 = b_acc[i];24.
int t3 = a_acc[i + 16];71

oneAPI GPU 最適化ガイド
25.
int t4 = b_acc[i + 16];26.
sum_acc[i] = t1 + t2;
27.
sum_acc[i + 16] = t3 + t4;
28.
}
29.
});
30.
});
31.
}
32.
q.wait();33.
auto end = std::chrono::steady_clock::now();34.
std::cout << "Vector add4 completed on device - took "35.
<< (end - start).count() << " u-secs\n";36.
return ((end - start).count());
37. } // VectorAdd4 の終わり

次のカーネルでは、restrict ディレクティブが追加されています。このディレクティブは、ループ内で書き込まれるベクトルにエイリアスがなく、前述の例では手動で行ったようなストアを超えてロードをホイスト可能であるヒントをコンパイラーに与えます。
1. size_t VectorAdd5(sycl::queue &q, const IntArray &a, const IntArray &b,
2.
IntArray &sum, int iter) {3.
sycl::range num_items{a.size()};4.
5.
sycl::buffer a_buf(a);6.
sycl::buffer b_buf(b);7.
sycl::buffer sum_buf(sum.data(), num_items);
8.
size_t num_groups = 1;9.
size_t wg_size = 16;
10.
auto start = std::chrono::steady_clock::now();11.
for (int i = 0; i < iter; i++) {12.
q.submit([&](auto &h) {13.
// 入力アクセサー
14.
sycl::accessor a_acc(a_buf, h, sycl::read_only);15.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
16.
// 出力アクセサー
17.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
18.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
19.
[=](sycl::nd_item<1> index) [[intel::reqd_sub_group_size(20.
16)]] [[intel::kernel_args_restrict]] {21.
// コンパイラーはロードをホイストする必要があります22.
size_t loc_id = index.get_local_id();23.
for (size_t i = loc_id; i < mysize; i += 32) {24.
sum_acc[i] = a_acc[i] + b_acc[i];25.
sum_acc[i + 16] = a_acc[i + 16] + b_acc[i + 16];
26.
}
27.
});
28.
});
29.
}
30.
q.wait();31.
auto end = std::chrono::steady_clock::now();32.
std::cout << "Vector add5 completed on device - took "33.
<< (end - start).count() << " u-secs\n";34.
return ((end - start).count());
35. } // VectorAdd5 の終わり

5-3-6

カーネル内のスレッド間での同期

カーネル内の work-item を同期してデータ交換や更新を行ったり、相互に連携して特定の順番でタスクを実行するにはさまざまな方法があります。次にその方法を示します。

72

oneAPI GPU 最適化ガイド• アクセサークラス
アクセサークラスは、バッファーおよびイメージデータ構造の取得と解放を指定します。それらが生成および破棄される場所に応じて、ランタイムは適切なデータ転送を行い、同期プリミティブを生成します。
•
アトミック操作SYCL* デバイスは、C++ アトミックの制限があるサブセットをサポートします。
•
フェンス
フェンス・プリミティブは、ロードとストアを順序付けするために使用されます。フェンスはセマンティクスの取得と解放、またはその両方を保持できます。
•
バリア
バリアは、それぞれのグループ内の work-item を同期するために使用されます。
•
階層的な並列ディスパッチ
計算を表現する階層並列処理モデルでは、work-group 内の同期は明示的な work-group バリア操作を使用せず、
parallel_for_work_item 関数呼び出しの複数インスタンスを介して明示的に行われます。
•
デバイスイベント
イベントは、カーネル関数内での非同期操作の完了待機に使用されます。
多くの場合、上記の同期イベントのいずれかを使用して同じ機能を表現できますが、効率とパフォーマンスが異なります。
••

アトミック操作
ローカルバリアとグローバルバリア

5-3-6-1

アトミック操作

アトミックはメモリーを介した work-item 間の通信で複数の work-item を同期できます。SYCL* アトミックは C++
アトミックに類似しており、アトミックで保護されるリソースへのアクセスが単一ユニットとして実行することを保証しています。アトミック操作のパフォーマンスと正当性には、次の要因が影響します。
•••

データタイプバッファーとローカルアドレス空間
ホスト、共有、デバイスに割り当てられた USM

5-3-6-1-1

アトミック操作のデータタイプ

次のカーネルは、すべての work-item がグローバル・アキュムレーターをアトミックに更新する SYCL* リダクション操作の実装を示しています。加算の入力データタイプと、リダクション操作が適用されるベクトルは整数です。このカーネルのパフォーマンスは、リダクションに使用されるブロック化など他の手法と比較しても妥当と言えます。
1.
2.
3.
4.
5.
6.
7.
8.
9.

q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);h.parallel_for(data_size, [=](auto index) {size_t glob_id = index[0];auto v = sycl::atomic_ref<int,sycl::memory_order::relaxed,
sycl::memory_scope::device, sycl::access::address_space::global_space>(
sum_acc[0]);73oneAPI GPU 最適化ガイド
10.
v.fetch_add(buf_acc[glob_id]);11.
});12. });

次のカーネルのようにベクトルのデータタイプが float または double である場合、float と double にはハードウェアによるアトミック操作のサポートがないことからアクセラレーターのパフォーマンスが低くなることがあります。次の 2 つのカーネルは、アトミック加算を実行する時間が、ネイティブでアトミック操作がサポートされるかどうかにより大幅に異なる可能性があることを示しています。
1. //2. int VectorInt(sycl::queue &q, int iter) {3.
VectorAllocator<int> alloc;
4.
AlignedVector<int> a(array_size, alloc);
5.
AlignedVector<int> b(array_size, alloc);
6.
InitializeArray<int>(a);7.
InitializeArray<int>(b);8.
sycl::range num_items{a.size()};9.
sycl::buffer a_buf(a);10.
sycl::buffer b_buf(b);11.
auto start = std::chrono::steady_clock::now();12.
for (int i = 0; i < iter; i++) {13.
q.submit([&](sycl::handler &h) {14.
// 入力アクセサー
15.
sycl::accessor a_acc(a_buf, h, sycl::read_write);16.
sycl::accessor b_acc(a_buf, h, sycl::read_only);
17.
h.parallel_for(num_items, [=](auto i) {18.
auto v = sycl::atomic_ref<int, sycl::memory_order::relaxed,
19.
sycl::memory_scope::device,20.
sycl::access::address_space::global_space>(21.
a_acc[0]);22.
v += b_acc[i];
23.
});
24.
});
25.
}
26.
q.wait();27.
auto end = std::chrono::steady_clock::now();28.
std::cout << "Vector int completed on device - took "29.
<< (end - start).count()
30.
<< " u-secs\n";31.
return ((end - start).count());
32. }アトミックを使用する場合、ハードウェアによるサポートがあり効率良く実行されるか確認する必要があります。
Gen9 およびインテル® Iris® Xe 統合グラフィックスでは、float と double データタイプのアトミックはサポートされていないため、VectorDouble のパフォーマンスは非常に低くなります。
1. //2. int VectorDouble(sycl::queue &q, int iter) {3.
VectorAllocator<double> alloc;
4.
AlignedVector<double> a(array_size, alloc);
5.
AlignedVector<double> b(array_size, alloc);
6.
InitializeArray<double>(a);7.
InitializeArray<double>(b);8.
sycl::range num_items{a.size()};9.
sycl::buffer a_buf(a);10.
sycl::buffer b_buf(b);11.
auto start = std::chrono::steady_clock::now();12.
for (int i = 0; i < iter; i++) {13.
q.submit([&](sycl::handler &h) {14.
// 入力アクセサー
15.
sycl::accessor a_acc(a_buf, h, sycl::read_write);16.
sycl::accessor b_acc(a_buf, h, sycl::read_only);
17.
h.parallel_for(num_items, [=](auto i) {18.
auto v = sycl::atomic_ref<double, sycl::memory_order::relaxed,74

oneAPI GPU 最適化ガイド
19.
sycl::memory_scope::device,20.
sycl::access::address_space::global_space>(21.
a_acc[0]);22.
v += b_acc[i];
23.
});
24.
});
25.
}
26.
q.wait();27.
auto end = std::chrono::steady_clock::now();28.
std::cout << "Vector Double completed on device - took "29.
<< (end - start).count() << " u-secs\n";30.
return ((end - start).count());
31. }インテル® VTune™ プロファイラーを使用してこれらのカーネルを解析すると、ネイティブアトミックの影響を測定することができます。VectorInt カーネルは、VectorDouble および VectorFloat カーネルよりもはるかに高速であることが分かります。
インテル® VTune™ プロファイラーにおける動的命令

インテル® VTune™ プロファイラーの動的命令解析を使用すると、ネイティブアトミックがサポートされていない場合、命令数が劇的に変化することが分かります。
以下に VectorInt カーネルのアセンブリー・コードを示します。
インテル® VTune™ プロファイラーにおけるアトミック整数

75

oneAPI GPU 最適化ガイド
VectorDouble のアセンブリー・コードと比較すると、VectorDouble カーネルの実行には、さらに 3,300 万のGPU 命令が必要であることが分かります。
インテル® VTune™ プロファイラーにおけるアトミック倍精度

76

oneAPI GPU 最適化ガイド
インテル® Advisor には、GPU カーネルのパフォーマンス向上の可能性を示す情報を提供する推奨ペインがあります。
インテル® Advisor の推奨ペイン

インテル® Advisor の推奨事項の 1 つに「非効率なアトミックが存在します」という項目があります。アトミック操作
がハードウェアでネイティブにサポートされていない場合、エミュレートされますが、インテル® Advisor はこれを検出可能であり、可能な解決策に関するアドバイスを提供します。
インテル® Advisor が示す非効率なアトミック

5-3-6-1-2

グローバルとローカルアドレス空間のアトミック操作

標準の C++ メモリーモデルは、アプリケーションが単一アドレス空間を持つ、単一デバイスで実行されることを前提としています。これらの仮定はどちらも SYCL* アプリケーションには当てはまりません。SYCL* では、アプリケーショ
ンの異なる領域を各種デバイス (ホストデバイスと 1 つ以上のアクセラレーター・デバイス) で実行できます。それぞれのデバイスには複数のアドレス空間 (プライベート、ローカル、グローバル) があります。また、デバイスのグローバル
アドレス空間は互いに分かれている場合があります (USM サポートに依存します)。

77oneAPI GPU 最適化ガイド
グローバルアドレス空間でアトミックを使用する場合、グローバルの更新はローカルよりも時間がかかるため注意が必要です。
1. #include <CL/sycl.hpp>
2. #include <iostream>
3. int main() {4.
constexpr int N = 256 * 256;5.
constexpr int M = 512;6.
int total = 0;
7.
int *a = static_cast<int *>(malloc(sizeof(int) * N));8.
for (int i = 0; i < N; i++)9.
a[i] = 1;
10.
sycl::queue q({sycl::property::queue::enable_profiling()});
11.
sycl::buffer<int> buf(&total, 1);12.
sycl::buffer<int> bufa(a, N);13.
auto e = q.submit([&](sycl::handler &h) {14.
sycl::accessor acc(buf, h);15.
sycl::accessor acc_a(bufa, h, sycl::read_only);
16.
h.parallel_for(sycl::nd_range<1>(N, M), [=](auto it) {17.
auto i = it.get_global_id();18.
sycl::atomic_ref<int, sycl::memory_order_relaxed,
19.
sycl::memory_scope_device,20.
sycl::access::address_space::global_space>21.
atomic_op(acc[0]);22.
atomic_op += acc_a[i];
23.
});
24.
});
25.
sycl::host_accessor h_a(buf);
26.
std::cout << "Reduction Sum : " << h_a[0] << "\n";27.
std::cout
28.
<< "Kernel Execution Time of Global Atomics Ref: "29.
<< e.get_profiling_info<sycl::info::event_profiling::command_end>() 30.
e.get_profiling_info<sycl::info::event_profiling::command_start>()31.
<< "\n";32.
return 0;33. }次の例のように、ローカルメモリー空間を使用するようにコードを再構成することができます。
1. #include <CL/sycl.hpp>
2. #include <iostream>
3. int main() {4.
constexpr int N = 256 * 256;5.
constexpr int M = 512;6.
constexpr int NUM_WG = N / M;
7.
int total = 0;8.
int *a = static_cast<int *>(malloc(sizeof(int) * N));9.
for (int i = 0; i < N; i++)10.
a[i] = 1;
11.
sycl::queue q({sycl::property::queue::enable_profiling()});
12.
sycl::buffer<int> global(&total, 1);13.
sycl::buffer<int> bufa(a, N);
14.
auto e1 = q.submit([&](sycl::handler &h) {15.
sycl::accessor b(global, h);16.
sycl::accessor acc_a(bufa, h, sycl::read_only);
17.
auto acc = sycl::local_accessor<int, 1>(NUM_WG, h);18.
h.parallel_for(sycl::nd_range<1>(N, M), [=](auto it) {19.
auto i = it.get_global_id(0);20.
auto group_id = it.get_group(0);21.
sycl::atomic_ref<int, sycl::memory_order_relaxed,
22.
sycl::memory_scope_device,23.
sycl::access::address_space::local_space>24.
atomic_op(acc[group_id]);25.
sycl::atomic_ref<int, sycl::memory_order_relaxed,
26.
sycl::memory_scope_device,27.
sycl::access::address_space::global_space>28.
atomic_op_global(b[0]);78oneAPI GPU 最適化ガイド
29.
atomic_op += acc_a[i];30.
it.barrier(sycl::access::fence_space::local_space);31.
if (it.get_local_id() == 0)32.
atomic_op_global += acc[group_id];
33.
});
34.
});
35.
sycl::host_accessor h_global(global);
36.
std::cout << "Reduction Sum : " << h_global[0] << "\n";37.
int total_time =38.
(e1.get_profiling_info<sycl::info::event_profiling::command_end>() 39.
e1.get_profiling_info<sycl::info::event_profiling::command_start>());
40.
std::cout << "Kernel Execution Time of Local Atomics : " << total_time
41.
<< "\n";42.
return 0;
43. }5-3-6-1-3

USM データ上のアトミック操作

ディスクリート GPU には以下が当てはまります。
•

ホストに割り当てられた USM (sycl::malloc_host) のアトミック操作はサポートされていません。

•

ホストとデバイスから共有 USM ロケーションへの同時アクセス (sycl::malloc_shared) はサポートされていません。

アトミック操作とデバイスのアルゴリズムには、デバイスに割り当てられた USM (sycl::malloc_device) メモ
リーを使用することを推奨します。

5-3-6-1-4

アトミック操作のメモリースコープ

アトミック操作のメモリースコープを使用すると、データ競合を回避できます。
sycl::memory_scope パラメーターの詳細については、SYCL* 仕様のメモリースコープのセクションを参照してください。
メモリーモデルの詳細については、DPC++ 書籍の 「メモリーモデルとアトミック」 の章を参照してください。

5-3-6-2

ローカルバリアとグローバルアトミック

アトミックを使用すると、カーネル内で複数の work-item が共有リソースを操作できます。バリアは、work-group 内
で work-item 間の同期を可能にします。カーネルの起動とローカルバリアを適切に使用することで、グローバルアト
ミック機能を実現できます。アーキテクチャーと関連するデータ量によって、どちらか一方のパフォーマンスが向上する可能性があります。
次の例では、ベクトル内の少量の要素を総計しようとしています。このタスクは個別に実行できます。以下のリストに示すカーネルは、ベクトルのすべての要素をウォークスルーして、単一の work-item のみがそれらの合計を行います。
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.

q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);h.parallel_for(data_size, [=](auto index) {int glob_id = index[0];if (glob_id == 0) {int sum = 0;
for (int i = 0; i < N; i++)sum += buf_acc[i];sum_acc[0] = sum;79

oneAPI GPU 最適化ガイド
11.
}
12.
});
13. });

以下のカーネルは、すべての work-item が合計する値をグローバル変数で更新するグローバルアトミックを使用して同じ問題を解決します。これは並列に実行されますが、グローバル変数の競合が非常に多く、ほとんどの場合パフォーマンスはそれほど期待できません。
1. q.submit([&](auto &h) {2.
sycl::accessor buf_acc(buf, h, sycl::read_only);3.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
4.
h.parallel_for(data_size, [=](auto index) {5.
size_t glob_id = index[0];6.
auto v = sycl::atomic_ref<int, sycl::memory_order::relaxed,
7.
sycl::memory_scope::device,8.
sycl::access::address_space::global_space>(9.
sum_acc[0]);10.
v.fetch_add(buf_acc[glob_id]);11.
});12. });

次のカーネルは、すべての work-item がベクトルの複数の要素を合計します。この合計は並列に行われ、workgroup のすべての work-item 間で共有される配列が更新されます。work-group のすべての work-item がバリアを使用してツリー・リダクションを行い、それらを同期して共有メモリーの中間結果を最終的にリダクションします。このカーネルは、明示的に 1 つの work-group を作成し、ベクトル内のすべての要素を更新する役割を work-group 内の work-item に分散します。スレッド数の観点からはマシンのリソースを使い切っていませんが、問題サイズが小さい場合はこれくらいの並列処理で十分なこともあります。
1. Timer timer;
2. q.submit([&](auto &h) {3.
sycl::accessor buf_acc(buf, h, sycl::read_only);4.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);5.
sycl::local_accessor<int, 1> scratch(work_group_size, h);6.
h.parallel_for(sycl::nd_range<1>{work_group_size, work_group_size},
7.
[=](sycl::nd_item<1> item) {8.
size_t loc_id = item.get_local_id(0);9.
int sum = 0;
10.
for (int i = loc_id; i < data_size; i += num_work_items)
11.
sum += buf_acc[i];12.
scratch[loc_id] = sum;13.
for (int i = work_group_size / 2; i > 0; i >>= 1) {14.
item.barrier(sycl::access::fence_space::local_space);15.
if (loc_id < i)16.
scratch[loc_id] += scratch[loc_id + i];
17.
}
18.
if (loc_id == 0)19.
sum_acc[0] = scratch[0];20.
});21. });

ここに示した 3 つのカーネルのパフォーマンスはプラットフォーム間でかなり異なり、開発者はアプリケーションと実行されるハードウェアに適した手法を選択する必要があります。

80

oneAPI GPU 最適化ガイド

5-3-7

work-group サイズを選択する考慮事項

SYCL* では nd_range カーネルの work-group サイズを選択できます。work-group のサイズは、計算リソース、
ベクトルレーン、および work-item 間の通信の利用率に影響を及ぼします。同じ work-group 内の work-item は、共有メモリーやハードウェアによる同期機能などのハードウェア・リソースを利用することがあります。これにより、
work-group 間で work-item よりも効率良く通信できるようになります。これには一般的に、アクセラレーターでサ
ポ ー ト さ れ る 最 大 work-group サ イ ズ を 選 択 す る 必 要 が あ り ま す 。 最 大 work-group サ イ ズ は 、device::get_info<cl::sycl::info::device::max_work_group_size>() を呼び出すことで照会できます。
work-group サイズの選択の影響を明確にするため、大きなベクトル全体のすべての要素を加算するリダクション・
カーネルを考えてみます。カーネルを実行する関数は、work-group-size と sub-group-size を引数として受け取り、異なる値の組み合わせで検証を行うことができるようにします。パフォーマンスの差は、さまざまな値の workgroup サイズを使用してカーネルが呼び出されたときにレポートされるタイミングから確認できます。
1. void reduction(sycl::queue &q, std::vector<int> &data, std::vector<int> &flush,2.
int iter, int work_group_size) {3.
const size_t data_size = data.size();4.
const size_t flush_size = flush.size();5.
int sum = 0;
6.
const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};7. // int vec_size =
8. // q.get_device().get_info<sycl::info::device::native_vector_width_int>();9.
int num_work_items = data_size / work_group_size;
10.
sycl::buffer<int> buf(data.data(), data_size, props);11.
sycl::buffer<int> flush_buf(flush.data(), flush_size, props);
12.
sycl::buffer<int> sum_buf(&sum, 1, props);
13.
init_data(q, buf, data_size);
14.
double elapsed = 0;
15.
for (int i = 0; i < iter; i++) {16.
q.submit([&](auto &h) {17.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
18.
h.parallel_for(1, [=](auto index) { sum_acc[index] = 0; });
19.
});
20.
// キャッシュをフラッシュ
21.
q.submit([&](auto &h) {22.
sycl::accessor flush_acc(flush_buf, h, sycl::write_only, sycl::no_init);
23.
h.parallel_for(flush_size, [=](auto index) { flush_acc[index] = 1; });
24.
});
25.
Timer timer;
26.
// reductionMapToHWVector main begin27.
q.submit([&](auto &h) {28.
sycl::accessor buf_acc(buf, h, sycl::read_only);29.
sycl::local_accessor<int, 1> scratch(work_group_size, h);30.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
31.
h.parallel_for(32.
sycl::nd_range<1>(num_work_items, work_group_size),
33.
[=](sycl::nd_item<1> item) [[intel::reqd_sub_group_size(16)]] {34.
auto v =35.
sycl::atomic_ref<int, sycl::memory_order::relaxed,
36.
sycl::memory_scope::device,37.
sycl::access::address_space::global_space>(38.
sum_acc[0]);39.
int sum = 0;40.
int glob_id = item.get_global_id();41.
int loc_id = item.get_local_id();42.
for (unsigned int i = glob_id; i < data_size; i += num_work_items)
43.
sum += buf_acc[i];44.
scratch[loc_id] = sum;45.
for (int i = work_group_size / 2; i > 0; i >>= 1) {46.
item.barrier(sycl::access::fence_space::local_space);47.
if (loc_id < i)81

oneAPI GPU 最適化ガイド
48.
scratch[loc_id] += scratch[loc_id + i];
49.
}
50.
if (loc_id == 0)51.
v.fetch_add(scratch[0]);
52.
});
53.
});
54.
q.wait();55.
elapsed += timer.Elapsed();56.
sycl::host_accessor h_acc(sum_buf);
57.
sum = h_acc[0];
58.
}
59.
elapsed = elapsed / iter;60.
std::string msg = "with work-groups=" + std::to_string(work_group_size);
61.
check_result(elapsed, msg, sum);62. } // reduction の終わり

次のコードでは、上記のカーネルは 2 つの異なる値 (2 * vec_size とアクセラレーターでサポートされる最大
work-group サイズ) で呼び出されます。work-group サイズが 2* vec_size と等しい場合、カーネルのパフォーマ
ンスは work-group サイズが最大値である場合よりも低くなります。
int vec_size = 16;
int work_group_size = vec_size;
reduction(q, data, extra, 16, work_group_size);
work_group_size =q.get_device().get_info<sycl::info::device::max_work_group_size>();reduction(q, data, extra, 16, work_group_size);

バリアやアトミックが利用されない場合、work-group サイズはパフォーマンスに影響しません。これを理解するため、アトミックやバリアがない vec_copy カーネルについて考えてみます。
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.

82

void vec_copy(sycl::queue &q, std::vector<int> &src, std::vector<int> &dst,
std::vector<int> &flush, int iter, int work_group_size) {const size_t data_size = src.size();const size_t flush_size = flush.size();const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};int num_work_items = data_size;
double elapsed = 0;{
sycl::buffer<int> src_buf(src.data(), data_size, props);
sycl::buffer<int> dst_buf(dst.data(), data_size, props);
sycl::buffer<int> flush_buf(flush.data(), flush_size, props);for (int i = 0; i < iter; i++) {// キャッシュをフラッシュq.submit([&](auto &h) {sycl::accessor flush_acc(flush_buf, h, sycl::write_only, sycl::no_init);h.parallel_for(flush_size, [=](auto index) { flush_acc[index] = 1; });});Timer timer;q.submit([&](auto &h) {sycl::accessor src_acc(src_buf, h, sycl::read_only);
sycl::accessor dst_acc(dst_buf, h, sycl::write_only, sycl::no_init);h.parallel_for(sycl::nd_range<1>(num_work_items, work_group_size),[=](sycl::nd_item<1> item) [[intel::reqd_sub_group_size(16)]] {int glob_id = item.get_global_id();
dst_acc[glob_id] = src_acc[glob_id];});});q.wait();elapsed += timer.Elapsed();}}elapsed = elapsed / iter;
std::string msg = "with work-group-size=" + std::to_string(work_group_size);check_result(elapsed, msg, dst);

oneAPI GPU 最適化ガイド
35. } // vec_copy の終わり

以下のコードでは、上記のカーネルがさまざまな work-group サイズで呼び出されます。上記のカーネル呼び出しはすべて同じランタイムによって行われます。これは、work-group サイズがパフォーマンスに影響しないことを示します。これは、work-group 内で生成されたスレッドとは異なる work-group のスレッドが、work-group にバリアや共
有メモリーがない場合、スケジュールとリソースの観点からは同じように動作することによります。
int vec_size = 16;
int work_group_size = vec_size;
vec_copy(q, src, dst, extra, 16, work_group_size);
work_group_size = 2 * vec_size;
vec_copy(q, src, dst, extra, 16, work_group_size);
work_group_size = 4 * vec_size;
vec_copy(q, src, dst, extra, 16, work_group_size);
work_group_size = 8 * vec_size;
vec_copy(q, src, dst, extra, 16, work_group_size);
work_group_size = 16 * vec_size;
vec_copy(q, src, dst, extra, 16, work_group_size);

一部のアクセラレーターでは、処理要素間でスレッドがスケジュールされるより、最小 sub-group サイズを適用する
ことで良好なパフォーマンスが得られます。そのような状況では、sub-group 数が最小値より小さい場合、大きなパフォーマンスの差が見られます。上記の 3 行目のカーネル呼び出しでは 1 つの sub-group しかありませんが、5 行目の呼び出しでは 2 つの sub-group があります。同時に 2 つの sub-group のスケジュールを行うアクセラレーター
では、それら 2 つのカーネル呼び出しのタイミングによって大幅なパフォーマンスの差が生じます。

5-3-7-1

OpenMP* オフロードモードにおけるローカルおよびグローバル work-group
サイズを使用したカーネルのチューニング

前述した SYCL* におけるアクセラレーター・デバイス向けのカーネルのパフォーマンス・チューニングに関する手法
は、OpenMP* を使用した実装にも適用できます。OpenMP* ディレクティブを使用してアプリケーション・カーネルを
カスタマイズすることで、適切な work-group サイズを利用できます。ただし、コードの大幅な修正が必要になる可能性があります。OpenMP* による実装では、環境変数を使用してカーネルをカスタマイズするオプションも用意されています。次に示すように、環境変数 OMP_THREAD_LIMIT と OMP_NUM_TEAMS は、ローカル・ワークグループ・サイ
ズ (LWS) とグローバル・ワークグループ・サイズ (GWS) を設定するのに役立ちます。
LWS = OMP_THREAD_LIMITGWS = OMP_THREAD_LIMIT * OMP_NUM_TEAMS

以下に、LWS と GWS を使用してアクセラレーター・デバイスでリダクション・カーネルをチューニングする方法を紹介します。
1. int N = 2048;
2. double* A = make_array(N, 0.8);
3. double* B = make_array(N, 0.65);
4. double* C = make_array(N*N, 2.5);
5. if ((A == NULL) || (B == NULL) || (C == NULL))
6.
exit(1);
7. int i, j;
8. double val = 0.0;
9. #pragma omp target map(to:A[0:N],B[0:N],C[0:N*N]) map(tofrom:val)
10. {11. #pragma omp teams distribute parallel for collapse(2) reduction(+ : val)
12.
for (i = 0; i < N; i++) {13.
for (j = 0; j < N; j++) {14.
val += C[i * N + j] * A[i] * B[j];
15.
}
16.
}
17. }

83

oneAPI GPU 最適化ガイド
18. printf("val = %f10.3\n", val);
19. free(A);20. free(B);21. free(C);

例: OMP_THREAD_LIMIT = 1024、OMP_NUM_TEAMS = 120 に設定すると、LWS と GWS パラメーターがそれぞれ1024 と 122880 に設定されます。

上図は、LWS = 1024、GWS = 30720 で、このカーネルの最高のパフォーマンスが得られることを示しています。
これは、OMP_THREAD_LIMIT = 1024、OMP_NUM_TEAMS = 30 に相当します。これらの環境変数は、OpenMP*
でオフロードされるすべてのカーネルに対して、LWS と GWS の値を固定値に設定します。ただし、これらの環境変数は、
oneMKL のような高度に調整されたライブラリー・カーネルで使用される LWS および GWS には影響しません。

5-3-8

プリフェッチグローバルメモリーへのアクセスは、実行ストールの最も一般的な原因の 1 つです。アプリケーションがストールして、データがローカルレジスターに到達するのを待機する遅延をレイテンシーと呼びます。これは、アクセスがキャッシュをミスして、HBM メモリーにアクセスする必要がある場合は特にコストがかかります。GPU は実行する命令の順
番を変更し (アウトオブオーダー実行)、ベクトルエンジンでスレッドを切り替えることでメモリーのレイテンシーを隠匿しようとします。これが十分に機能せず、アプリケーションがストールし続け、メモリー帯域幅が十分に活用されな
い場合、プリフェッチ機能を使用して使用される前にデータをローカルキャッシュにロードするタイミングのヒントを与える場合があります。これを適切に使用すると、以降のメモリーアクセスがキャッシュにヒットし、データがレジス
ターに到達する距離が短縮され、メモリー待機の時間が短縮されます。プリフェッチは、インテル® データセンター
GPU マックス・シリーズ以降の製品で機能します。
プリフェッチは非同期操作であり、プリフェッチを送信するとアプリケーションはデータの到達を待機せず実行を続けます。これは、予期されるアクセスよりもかなり前にプリフェッチを送信する必要があることを意味します。HBM メモ
リーからローカルキャッシュの間には、データ転送を隠匿するのに十分な命令が存在します。
プリフェッチの一般的な使用例として、ループ内のメモリーアクセスがあります。通常、コンパイラーはループ展開の最適化を試みますが、これによりスケジューラーに実行する命令が与えられ、ループ反復が複合されプロセス内のメ
84

oneAPI GPU 最適化ガイド
モリー・レイテンシーが隠匿されます。この方法を適用できない (ループ境界が不明、大規模なカーネルの高いレジス
ター・プレッシャー)、プリフェッチを使用することで、以降の反復向けにデータをキャッシュすることでメモリー待機時間を短縮できます。
インテル® VTune™ プロファイラーを使用して、プリフェッチを適用する可能性を調査できます。メモリー帯域幅が十分に活用されておらず、スコアボード・ストール数が多き場合 (データがロードされるまで実行がストールしている)、
プリフェッチを適切に挿入することで実行速度が向上する可能性があります。
このセクションの例では、GPU がレイテンシーを隠匿できない場合をシミュレートするシナリオを考えます。最初にwork-group を 1 つだけ実行し、ハードウェア・スレッドを 1 つのベクトルエンジン (VE) に送信します。次に、pragma
を使用してループ展開を無効にします。これにより、メモリーの待機時間を隠匿するため、再スケジュールできる命令数が制限されます。
最初の例では、カーネルは配列の値をレデュースして結果を result に保存します。値は間接インデックスを使用し、インデックスは配列インデックスからからロードされます。インデックスはコンパイル時に不明であるため、コンパイ
ラーは配列のアクセスを最適化できません。ループのアンロールが無効化されているため、GPU はインデックスと値にアクセスする間に他のワークをスケジュールすることができず、インデックスへのアクセスが終了するまで実行はス
トールします。
1. auto e = q.submit([&](auto &h) {2.
h.parallel_for(
3.
sycl::nd_range(sycl::range{GLOBAL_WORK_SIZE},
4.
sycl::range{GLOBAL_WORK_SIZE}),
5.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(SIMD_SIZE)]] {6.
const int i = it.get_global_linear_id();7.
const int lane = it.get_sub_group().get_local_id()[0];8.
const int subgroup = it.get_sub_group().get_group_id()[0];9.
10.
// Index starting position11.
int *indexCurrent = indices + lane + subgroup * ITEMS_PER_SUBGROUP;
12.
13.
float dx = 0.0f;
14. #pragma unroll(0)15.
for (int j = 0; j < ITEMS_PER_LANE; ++j) {16.
// Load index for indirect addressing
17.
int index = *indexCurrent;18.
// Waits for load to finish, high latency
19.
float v = values[index];20.
for (int k = 0; k < 64; ++k)21.
dx += sqrtf(v + k);
22.
23.
indexCurrent += SIMD_SIZE;
24.
}
25.
26.
result[i] = dx;
27.
});
28.
});

2 番目の例では、インデックスから現在の値にアクセスする前に、カーネルは sycl::global_ptr::prefetch を呼び出して次のインデックスのプリフェッチを要求しているため、次の反復でのアクセスが高速になる可能性があります。レイテンシーを隠匿するのに十分な数の命令がないため、最初の反復ではデータはプリフェッチされません。
1.
2.
3.
4.
5.
6.
7.
8.

auto e = q.submit([&](auto &h) {h.parallel_for(sycl::nd_range(sycl::range{GLOBAL_WORK_SIZE},
sycl::range{GLOBAL_WORK_SIZE}),[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(SIMD_SIZE)]] {const int i = it.get_global_linear_id();
const int lane = it.get_sub_group().get_local_id()[0];const int subgroup = it.get_sub_group().get_group_id()[0];85oneAPI GPU 最適化ガイド
9.
10.
// Index starting position11.
int *indexCurrent = indices + lane + subgroup * ITEMS_PER_SUBGROUP;
12.
int *indexNext = indexCurrent + SIMD_SIZE;
13.
14.
float dx = 0.0f;
15. #pragma unroll(0)16.
for (int j = 0; j < ITEMS_PER_LANE; ++j) {17.
// Prefetch next index to cache18.
sycl::global_ptr<int>(indexNext).prefetch(1);19.
// Load index, might be cached20.
int index = *indexCurrent;21.
// Latency might be reduced if index was cached22.
float v = values[index];23.
for (int k = 0; k < 64; ++k)24.
dx += sqrtf(v + k);
25.
26.
indexCurrent = indexNext;
27.
indexNext += SIMD_SIZE;
28.
}
29.
30.
result[i] = dx;
31.
});
32.
});

sycl::global_ptr::prefetch は、プリフェッチする連続する要素数を示す引数を 1 つ受け取ります。プリフェッチはグローバルアドレス空間でのみ使用でき、この関数には考慮すべきいくつかの制限事項があります。
1.
2.
3.

コンパイラーは、各プリフェッチ呼び出しに対し、work-item あたり最大 32 バイトをキャッシュします (大きなデータタイプに対しても)。
コンパイラーには、コンパイル時に引数 (プリフェッチする連続する要素数) が定数であることが最適です。
どのキャッシュのデータをプリフェッチするかは制御できません。現在の実装では、データは L3 キャッシュにのみロードされます。

キャッシュの制御が十分でないと、sycl::global_ptr::prefetch によってパフォーマンスが向上する可能性が制限されます。2 番目の例を実行すると、パフォーマンスが低下する場合もあります。データが L1 キャッシュにロードされる場合、プリフェッチは最良の結果をもたらします。現在、インテル® データセンター GPU マックス・シリーズ専用の機能でのみ可能です。まず、これらの宣言をコードに追加します。
1. #include <CL/sycl.hpp>
2.
3. enum LSC_LDCC {4.
LSC_LDCC_DEFAULT,
5.
LSC_LDCC_L1UC_L3UC, // 1 // Override to L1 uncached and L3 uncached6.
LSC_LDCC_L1UC_L3C, // 2 // Override to L1 uncached and L3 cached7.
LSC_LDCC_L1C_L3UC, // 3 // Override to L1 cached and L3 uncached8.
LSC_LDCC_L1C_L3C,// 4 // Override to L1 cached and L3 cached9.
LSC_LDCC_L1S_L3UC, // 5 // Override to L1 streaming load and L3 uncached10.
LSC_LDCC_L1S_L3C,// 6 // Override to L1 streaming load and L3 cached11.
LSC_LDCC_L1IAR_L3C, // 7 // Override to L1 invalidate-after-read, and L3
12.
// cached
13. };
14.
15. extern "C" {16. SYCL_EXTERNAL void
17. __builtin_IB_lsc_prefetch_global_uchar(const __attribute__((opencl_global))
18.
uint8_t *base,19.
int immElemOff, enum LSC_LDCC cacheOpt);
20. SYCL_EXTERNAL void
21. __builtin_IB_lsc_prefetch_global_ushort(const __attribute__((opencl_global))
22.
uint16_t *base,23.
int immElemOff, enum LSC_LDCC cacheOpt);
24. SYCL_EXTERNAL void86

oneAPI GPU 最適化ガイド
25. __builtin_IB_lsc_prefetch_global_uint(const __attribute__((opencl_global))
26.
uint32_t *base,27.
int immElemOff, enum LSC_LDCC cacheOpt);
28. SYCL_EXTERNAL void
29. __builtin_IB_lsc_prefetch_global_uint2(const __attribute__((opencl_global))
30.
uint32_t *base,31.
int immElemOff, enum LSC_LDCC cacheOpt);
32. SYCL_EXTERNAL void
33. __builtin_IB_lsc_prefetch_global_uint3(const __attribute__((opencl_global))
34.
uint32_t *base,35.
int immElemOff, enum LSC_LDCC cacheOpt);
36. SYCL_EXTERNAL void
37. __builtin_IB_lsc_prefetch_global_uint4(const __attribute__((opencl_global))38.
uint32_t *base,39.
int immElemOff, enum LSC_LDCC cacheOpt);
40. SYCL_EXTERNAL void
41. __builtin_IB_lsc_prefetch_global_uint8(const __attribute__((opencl_global))42.
uint32_t *base,43.
int immElemOff, enum LSC_LDCC cacheOpt);
44. SYCL_EXTERNAL void
45. __builtin_IB_lsc_prefetch_global_ulong(const __attribute__((opencl_global))46.
uint64_t *base,47.
int immElemOff, enum LSC_LDCC cacheOpt);
48. SYCL_EXTERNAL void
49. __builtin_IB_lsc_prefetch_global_ulong2(const __attribute__((opencl_global))50.
uint64_t *base,51.
int immElemOff, enum LSC_LDCC cacheOpt);
52. SYCL_EXTERNAL void
53. __builtin_IB_lsc_prefetch_global_ulong3(const __attribute__((opencl_global))54.
uint64_t *base,55.
int immElemOff, enum LSC_LDCC cacheOpt);
56. SYCL_EXTERNAL void
57. __builtin_IB_lsc_prefetch_global_ulong4(const __attribute__((opencl_global))58.
uint64_t *base,59.
int immElemOff, enum LSC_LDCC cacheOpt);
60. SYCL_EXTERNAL void
61. __builtin_IB_lsc_prefetch_global_ulong8(const __attribute__((opencl_global))62.
uint64_t *base,63.
int immElemOff, enum LSC_LDCC cacheOpt);
64. }引 数 cacheOpt で は 対 象 の キ ャ ッ シ ュ を 選 択 し ま す 。 sycl::global_ptr::prefetch は
LSC_LDCC_L1UC_L3C にマップされます。ほとんどの場合、LSC_LDCC_L1UC_L3C は最良の結果をもたらします。
引数 immElemOff でベースポインターをオフセットできますが、通常ベースポインターはフェッチする適切なアドレスに設定され immElemOff は 0 になります。
L1 へのプリフェッチは、安全な境界外へのアクセスをサポートしません。このタイプのプリフェッチを行う場合、アプリ
ケーションは境界チェックを行う安全な所有権を取得する必要があります。そうしないと、境界外へのアクセスは未定
義の動作を引き起こします。
3 番目の例は、境界チェックを含む L1 プリフェッチの動作を示します。
1. auto e = q.submit([&](auto &h) {2.
h.parallel_for(
3.
sycl::nd_range(sycl::range{GLOBAL_WORK_SIZE},
4.
sycl::range{GLOBAL_WORK_SIZE}),
5.
[=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(SIMD_SIZE)]] {6.
const int i = it.get_global_linear_id();7.
const int lane = it.get_sub_group().get_local_id()[0];8.
const int subgroup = it.get_sub_group().get_group_id()[0];9.
10.
// Index starting position11.
int *indexCurrent = indices + lane + subgroup * ITEMS_PER_SUBGROUP;
12.
int *indexNext = indexCurrent + SIMD_SIZE;

87

oneAPI GPU 最適化ガイド
13.
14.
float dx = 0.0f;
15. #pragma unroll(0)16.
for (int j = 0; j < ITEMS_PER_LANE; ++j) {17.
// Prefetch next index to cache
18. #if __SYCL_DEVICE_ONLY__
19.
if (j < ITEMS_PER_LANE - 1)20.
__builtin_IB_lsc_prefetch_global_uint(
21.
(const __attribute__((opencl_global)) uint32_t *)indexNext, 0,22.
LSC_LDCC_L1C_L3C);
23. #endif
24.
// Load index, might be cached25.
int index = *indexCurrent;26.
// Latency might be reduced if index was cached27.
float v = values[index];28.
for (int k = 0; k < 64; ++k)29.
dx += sqrtf(v + k);30.
31.
indexCurrent = indexNext;
32.
indexNext += SIMD_SIZE;
33.
}
34.
35.
result[i] = dx;
36.
});
37.
});

データを手動でキャッシュにプリフェッチする場合、メモリーフェンスによってキャッシュがフラッシュされる可能性があるため、キャッシュに再度データを配置するためプリフェッチを繰り返すことが必要になることに注意してください。
ここで示した例は、プリフェッチの使用法に関するアイディアを提供するだけであり、実際の使い方を表すものではありません。このようなカーネルは、ループのアンロールのような通常の最適化手法で、メモリー・レイテンシーを隠匿するには十分です。プリフェッチのヒントは、コンパイラーによって適用される通常の最適化では不十分な大規模なカー
ネルでパフォーマンスを向上することが期待されます。

5-3-9

リダクション

リダクションは、配列のすべての要素に演算子を適用し、単一の結果を生成する並列プログラミングにおける一般的な操作です。リダクション演算子は結合可能であり、場合によっては可換です。リダクション演算子には、合計、最大、最小などがあります。以下にシリアル・リダクション合計の例を示します。
for (int it = 0; it < iter; it++) {sum = 0;for (size_t i = 0; i < data_size; ++i) {sum += data[i];}}リダクションの時間あたりの計算量は、要素数に比例します。これを並列化するにはいくつかの方法がありますが、異なる処理要素間の通信/同期を最小限にするように注意する必要があります。リダクションを並列化する最も単純な方法は、グローバル変数を使用して、スレッドがアトミック操作でこの変数を更新することです。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);h.parallel_for(data_size, [=](auto index) {size_t glob_id = index[0];auto v = sycl::atomic_ref<int, sycl::memory_order::relaxed,
sycl::memory_scope::device,
sycl::access::address_space::global_space>(
sum_acc[0]);88oneAPI GPU 最適化ガイド
v.fetch_add(buf_acc[glob_id]);});

スレッドが同一のメモリー位置をアトミックに更新して大量の競合が発生するため、このカーネルのパ
フォーマンスは低下します。より良いアプローチは、配列を小さなチャンクに分割し、それぞれのスレッド
が各チャンクのローカル合計を計算してから、ローカル合計をシーケンシャルなツリー・リダクションを行うことです。チャンク数はプラットフォームの処理要素数によって異なります。処理要素数は、デバイス・オ
ブジ ェ ク トの g et_ inf o <inf o:: dev ice :: ma x_co mpu te_ uni ts >( ) 関 数で 照 会で き ます 。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor accum_acc(accum_buf, h, sycl::write_only, sycl::no_init);h.parallel_for(num_processing_elements, [=](auto index) {size_t glob_id = index[0];size_t start = glob_id * BATCH;
size_t end = (glob_id + 1) * BATCH;
if (end > N)
end = N;int sum = 0;
for (size_t i = start; i < end; ++i)
sum += buf_acc[i];accum_acc[glob_id] = sum;});});

このカーネルは、共有メモリーをアトミックに更新するカーネルよりも高いパフォーマンスを示します。ただし、コンパイラーがループをベクトル化できないため、まだ十分とは言えません。コンパイラーがベクトルコードを生成できるようにする 1 つの方法は、次のようにループを変更することです。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor accum_acc(accum_buf, h, sycl::write_only, sycl::no_init);h.parallel_for(num_work_items, [=](auto index) {size_t glob_id = index[0];int sum = 0;
for (size_t i = glob_id; i < data_size; i += num_work_items)sum += buf_acc[i];accum_acc[glob_id] = sum;});});

コンパイラーはこのコードをベクトル化し、パフォーマンスを改善できます。
GPU では、マシン上の物理プロセッサー (ベクトルエンジン (VE) またはエグゼキューション・ユニット (EU) と呼ばれる) ごとに多くのスレッド・コンテキストを利用できます。そのため、スレッド数が VE 数と等しい上記のコードは、すべてのスレッド・コンテキストを利用できるわけではなく、コアごとに 2 つの論理スレッドを持つ CPU でも、コードがす
べてのスレッド・コンテキストを使用できるわけではありません。一般的に、すべてのスレッド・コンテキストを完全に利用するには、ワークを十分な数の work-group に分割します。これにより、コードは長いレイテンシーの命令を許容できるようになります。次の表は、各種デバイスの処理要素ごとに利用可能なスレッド・コンテキストの数を示しています。
デバイスで利用可能なスレッド・コンテキスト数
KBL✝
TGL✝

VE 数
24
96

VE ごとのスレッド数 | 合計スレッド数
7 | 24 x 7 = 168
7 | 96 x 7 = 672

89

oneAPI GPU 最適化ガイド
次のコードは、使用可能なリソースを完全に利用するため、十分なスレッドを持つカーネルを記述する方法を示しています。デバイスで利用可能なスレッド・コンテキストの数を照会する方法がないことに注意してください。そのため、デバイスに応じてワークを分割するため生成する work-item の数をスケーリングするようにします。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor accum_acc(accum_buf, h, sycl::write_only, sycl::no_init);h.parallel_for(num_work_items, [=](auto index) {size_t glob_id = index[0];int sum = 0;
for (size_t i = glob_id; i < data_size; i += num_work_items)sum += buf_acc[i];accum_acc[glob_id] = sum;});});

GPU でリダクションを行う一般的な手段として、多くの work-group を生成して、それぞれの work-group でツリー・リダクションを実行する方法があります。以下のカーネルでは、work-group 内の各 work-item がリダクションのネットワークに参加して、その work-group 内のすべての要素を合計しています。次に、work-group からのすべての中間結果のシリアル・リダクションを実行して合計します (中間結果が十分に大きい場合、ツリー・リダクションをさらに数回実行できます)。このツリー・リダクションのアルゴリズムは、work-group 内の work-item 間で高速な同期操作を利用します。このカーネルのパフォーマンスは、大量のカーネルが起動されるため、カーネルの起動効率に大きく依存します。また、実際にワークを実行するスレッド数はループを通過するごとに指数関数的に減少するため、以下のカーネルはそれほど効率的ではありません。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor accum_acc(accum_buf, h, sycl::write_only, sycl::no_init);
sycl::local_accessor<int, 1> scratch(work_group_size, h);h.parallel_for(sycl::nd_range<1>(num_work_items, work_group_size),[=](sycl::nd_item<1> item) {size_t global_id = item.get_global_id(0);int local_id = item.get_local_id(0);int group_id = item.get_group(0);if (global_id < data_size)
scratch[local_id] = buf_acc[global_id];
elsescratch[local_id] = 0;// work-group 内の項目でツリー・リダクションを行いますfor (int i = work_group_size / 2; i > 0; i >>= 1) {item.barrier(sycl::access::fence_space::local_space);if (local_id < i)scratch[local_id] += scratch[local_id + i];}
if (local_id == 0)
accum_acc[group_id] = scratch[0];});});単一ステージのリダクションは、ホストに多くのワークが残るため、あまり効率良いとは言えません。ステージを追加
すると、ホストのワークが減り、パフォーマンスが大幅に向上します。次のカーネルでは、ステージ 1 で計算された中間結果がステージ 2 の入力であることが分かります。これを一般化して、結果がホストで実行できるよう小さくなるまで、複数ステージのリダクションを構成します。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor accum_acc(accum1_buf, h, sycl::write_only, sycl::no_init);
sycl::local_accessor<int, 1> scratch(work_group_size, h);90

oneAPI GPU 最適化ガイド
h.parallel_for(sycl::nd_range<1>(num_work_items1, work_group_size),[=](sycl::nd_item<1> item) {size_t global_id = item.get_global_id(0);int local_id = item.get_local_id(0);int group_id = item.get_group(0);if (global_id < data_size)
scratch[local_id] = buf_acc[global_id];
elsescratch[local_id] = 0;// work-group 内の項目でツリー・リダクションを行いますfor (int i = work_group_size / 2; i > 0; i >>= 1) {item.barrier(sycl::access::fence_space::local_space);if (local_id < i)scratch[local_id] += scratch[local_id + i];}
if (local_id == 0)
accum_acc[group_id] = scratch[0];});});q.submit([&](auto &h) {sycl::accessor buf_acc(accum1_buf, h, sycl::read_only);
sycl::accessor accum_acc(accum2_buf, h, sycl::write_only, sycl::no_init);
sycl::local_accessor<int, 1> scratch(work_group_size, h);h.parallel_for(sycl::nd_range<1>(num_work_items2, work_group_size),[=](sycl::nd_item<1> item) {size_t global_id = item.get_global_id(0);int local_id = item.get_local_id(0);int group_id = item.get_group(0);if (global_id < static_cast<size_t>(num_work_items2))
scratch[local_id] = buf_acc[global_id];
elsescratch[local_id] = 0;// work-group 内の項目でツリー・リダクションを行いますfor (int i = work_group_size / 2; i > 0; i >>= 1) {item.barrier(sycl::access::fence_space::local_space);if (local_id < i)scratch[local_id] += scratch[local_id + i];}
if (local_id == 0)
accum_acc[group_id] = scratch[0];});});SYCL* はビルトインのリダクション操作をサポートしており、その実装はベースとなるアーキテクチャーに合わせて調整されるため、を使用する必要応じて SYCL* のリダクションを使用してください。次のカーネルは、コンパイラーがビ
ルトイン・リダクション操作を使用する方法を示します。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
auto sumr = sycl::reduction(sum_buf, h, sycl::plus<>());
h.parallel_for(sycl::nd_range<1>{data_size, 256}, sumr,[=](sycl::nd_item<1> item, auto &sumr_arg) {int glob_id = item.get_global_id(0);sumr_arg += buf_acc[glob_id];
});
});

91

oneAPI GPU 最適化ガイド
次の最適化として、入力ベクトルのアクセスをブロック化し、共有ローカルメモリーに中間結果を格納することが考えられます。このカーネルを以下に示します。ここでは、すべての work-item が特定数のベクトル要素を操作し、workgroup 内の 1 つのスレッドが中間結果を含む共有メモリーをリニアに処理することで、すべての要素を 1 つの結果に集約できます。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor accum_acc(accum_buf, h, sycl::write_only, sycl::no_init);
sycl::local_accessor<int, 1> scratch(work_group_size, h);h.parallel_for(sycl::nd_range<1>{num_work_items, work_group_size},[=](sycl::nd_item<1> item) {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);int offset = ((glob_id >> log2workitems_per_block)<< log2elements_per_block) +(glob_id & mask); int sum = 0;
for (int i = 0; i < elements_per_work_item; ++i)
sum +=
buf_acc[(i << log2workitems_per_block) + offset];
scratch[loc_id] = sum;// シリアル・リダクション
item.barrier(sycl::access::fence_space::local_space);if (loc_id == 0) {int sum = 0;
for (int i = 0; i < work_group_size; ++i)
sum += scratch[i];accum_acc[group_id] = sum;
}
});
});

以下のカーネルは、ツリー・リダクションにより work-group 内のすべての work-item から中間結果をリダクションすることを除き、上記のカーネルと同じです。ほとんどの場合、これによるパフォーマンスの影響はありません。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
sycl::accessor accum_acc(accum_buf, h, sycl::write_only, sycl::no_init);
sycl::local_accessor<int, 1> scratch(work_group_size, h);h.parallel_for(sycl::nd_range<1>{num_work_items, work_group_size},[=](sycl::nd_item<1> item) {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);int offset = ((glob_id >> log2workitems_per_block)<< log2elements_per_block) +(glob_id & mask);
int sum = 0;
for (int i = 0; i < elements_per_work_item; ++i)
sum +=
buf_acc[(i << log2workitems_per_block) + offset];
scratch[loc_id] = sum;// ツリー・リダクション
item.barrier(sycl::access::fence_space::local_space);for (int i = work_group_size / 2; i > 0; i >>= 1) {item.barrier(sycl::access::fence_space::local_space);if (loc_id < static_cast<size_t>(i))
scratch[loc_id] += scratch[loc_id + i];}
if (loc_id == 0)
accum_acc[group_id] = scratch[0];
});
});

92

oneAPI GPU 最適化ガイド
次のカーネルは、ブロック化を適用してからコンパイラーのリダクション演算子で最終的なリダクションを行います。
これは、テストを行ったほとんどのプラットフォームで良好なパフォーマンスを得られました。
q.submit([&](auto &h) {sycl::accessor buf_acc(buf, h, sycl::read_only);
auto sumr = sycl::reduction(sum_buf, h, sycl::plus<>());
h.parallel_for(sycl::nd_range<1>{num_work_items, work_group_size}, sumr,[=](sycl::nd_item<1> item, auto &sumr_arg) {size_t glob_id = item.get_global_id(0);int offset = ((glob_id >> log2workitems_per_block)<< log2elements_per_block) +(glob_id & mask);
int sum = 0;
for (int i = 0; i < elements_per_work_item; ++i)
sum +=
buf_acc[(i << log2workitems_per_block) + offset];
sumr_arg += sum;});});

次のカーネルは、完全に異なるメモリーアクセス手法を使用します。中間結果をベクトル形式で生成するため、subgroup のロードを使用します。次に、この中間結果をホストに戻し、ホスト上で最終的なリダクションが行われます。
別のカーネルを生成してこの結果を 1 つの work-group にまとめ、効率良いバリアを介してツリー・リダクションを行うことが適しているケースもあります。
q.submit([&](auto &h) {const sycl::accessor buf_acc(buf, h);sycl::accessor accum_acc(accum_buf, h, sycl::write_only, sycl::no_init);
sycl::local_accessor<sycl::vec<int, 8>, 1l> 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 group_id = item.get_group(0);size_t loc_id = item.get_local_id(0);sycl::sub_group sg = item.get_sub_group();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.get_group_id()[0] * sg.get_local_range()[0]) *elements_per_work_item;for (int i = 0; i < elements_per_work_item / 8; ++i)
sum += sg.load<8>(global_ptr(&buf_acc[base + i * 128]));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 < static_cast<size_t>(i))
scratch[loc_id] += scratch[loc_id + i];}
if (loc_id == 0)
accum_acc[group_id] = scratch[0];});});ここでは、アクセラレーターのアーキテクチャーに応じて異なるパフォーマンス特性を持つ、リダクション操作の各種実装方法を説明します。ここで注意すべきもう 1 つの重要な点は、PCIe* インターフェイス (ディスクリート GPU の場合) を経由するホストへのリダクションにかかる時間は、デバイスで実際にリダクション全体を実行するのとほぼ同じであるということです。これは、ホストとデバイス間のデータ転送を可能な限り避けるか、カーネルの実行をデータ転送とオーバーラップさせる必要があることを示しています。

93

oneAPI GPU 最適化ガイド

5-3-10カーネルの起動SYCL* では、特定デバイスのキューにカーネルを送信することでワークが実行されます。カーネルはホストからデバイ
スに送信され、デバイスで実行された結果が返されます。ホストからのカーネル送信と実行開始にはタイムラグがあります。これらは同期的であるため、カーネルに関連付けられる次のタイミングを追跡する必要があります。
•

カーネル送信開始時間
これは、ホストがカーネル送信処理を開始する時間です。

•

カーネル送信終了時間
これは、ホストがカーネル送信を終了する時間です。ホストは、引数のキューイングやカーネルがデバイスで実行を開始する際のランタイムリソース割り当てなど、複数のタスクを実行します。

•

カーネル起動時間
これは、ホストが送信したカーネルがデバイスで実行を開始する時間です。カーネル送信終了時間と同一でないことに注意してください。カーネル送信終了時間と起動時間には、デバイスの可用性に応じて遅延が生じます。
カーネルがデバイスで起動される前に、ホストは多数のカーネルをキューに投入する可能性があります。さらに、カーネルが実行を開始する前にデータ転送を完了しなければならないことがあります。

•

カーネル完了時間
これは、カーネルがデバイス上で実行を終了する時間です。現世代のデバイスはプリエンプティブではありません。
つまり、カーネルを起動したらその実行を完了する必要があります。

インテル® VTune™ プロファイラー (vtune) および unitrace などのツールは、アプリケーション内のすべてのカーネルに関する上記の時間の視覚的なタイムラインを表示できます。
次の簡単な例では、カーネルの実行時間を測定しています。これには、ホストでのカーネル送信時間、デバイスでのカーネル実行時間、およびデータ転送時間 (ここではバッファーやメモリーがないためゼロです) が含まれます。
void emptyKernel1(sycl::queue &q) {Timer timer;for (int i = 0; i < iters; ++i)
q.parallel_for(1, [=](auto) {/* NOP */}).wait();
std::cout << " emptyKernel1: Elapsed time: " << timer.Elapsed() / iters<< " sec\n";} // emptyKernel1 の終わり

parallel_for の終わりで待機しない次のコードでは、ホストがカーネルをランタイムに送信するのにかかる時間を測定します。
void emptyKernel2(sycl::queue &q) {Timer timer;for (int i = 0; i < iters; ++i)
q.parallel_for(1, [=](auto) {/* NOP */});std::cout << " emptyKernel2: Elapsed time: " << timer.Elapsed() / iters<< " sec\n";}オーバーヘッドは、使用するバックエンド・ランタイムとホストの処理能力に大きく依存します。

94

oneAPI GPU 最適化ガイド
デバイスでの実際のカーネル実行時間を測定するには、SYCL* ビルトイン・プロファイル API を使用します。次のコードは、カーネルの実行時間をプロファイルする SYCL* プロファイル API の使い方を示します。カーネルの送信時間も表示できます。カーネルの起動時間はランタイムとデバイスドライバーに依存するため、プログラムで測定する方法がありません。これを理解するにはプロファイル・ツールを使用します。
#include <CL/sycl.hpp>
class Timer {public:Timer() : start_(std::chrono::steady_clock::now()) {}double Elapsed() {auto now = std::chrono::steady_clock::now();
return std::chrono::duration_cast<Duration>(now - start_).count();}
private:using Duration = std::chrono::duration<double>;
std::chrono::steady_clock::time_point start_;};
int main() {Timer timer;
sycl::queue q{sycl::property::queue::enable_profiling()};auto evt = q.parallel_for(1000, [=](auto) {/* カーネルをここに記述 */});double t1 = timer.Elapsed();evt.wait();double t2 = timer.Elapsed();auto startK =evt.get_profiling_info<sycl::info::event_profiling::command_start>();auto endK =evt.get_profiling_info<sycl::info::event_profiling::command_end>();std::cout << "Kernel submission time: " << t1 << "secs\n";std::cout << "Kernel submission + execution time: " << t2 << "secs\n";std::cout << "Kernel execution time: "<< ((double)(endK - startK)) / 1000000.0 << "secs\n";return 0;}

次の図は、上記の例の実行のタイムラインを示しています。この図は、unitrace を実行してトレースファイルを生成し、ブラウザーでタイムラインを視覚化できます。このタイムラインには、ホスト側とデバイス側の 2 つのスイムレー
ンがあります。デバイス側のアクティビティーは、送信されたカーネルの実行のみであることに注意してください。
カーネルを実行できるように準備するために、ホスト側でかなりの量の作業が行われます。この場合、カーネルは非常 に 小 さ い た め 、 合 計 実 行 時 間 は カ ー ネ ル の JIT コ ン パ イ ル に よ っ て 支 配 さ れ ま す 。 こ れ は 、 下 の 図 で
zeModuleCreate というラベルの付いたブロックです。
カーネル実行のタイムライン

95

oneAPI GPU 最適化ガイド
また、ホストでのカーネル送信の完了とデバイスでのカーネルの実際の起動の間には遅延があることにも注意してください。

5-3-11

デバイス上で同時に複数のカーネルを実行

SYCL* には、カーネルを作成して実行のため送信に利用できる 2 種類のキューが用意されています。
•

インオーダー・キュー
カーネルはキューに送信された順に実行されます。

•

アウトオブオーダー・キュー
カーネルは任意の順に実行されます (カーネル間の依存関係の制約に従います)。

インオーダーまたはアウトオブオーダーでキューを作成するかどうかは、キューの作成時に
sycl::property::queue::in_order() プ ロ パ テ ィ ー を 使 用 し て 選 択 し ま す 。 デ フ ォ ル ト で 、 プ ロ パ
ティーが省略された場合のキューはアウトオブオーダーです。
次の例では、反復ごとに 3 つのカーネルが送信されています。それぞれのカーネルは、256 個の work-item を持つ 1
つの work-group のみを使用します。このカーネルは、マシン全体を占有しないようにするため 1 つのグループで生成されています。これは並列カーネル実行の利点を示すために行われます。
1. int multi_queue(sycl::queue &q, const IntArray &a, const IntArray &b) {2.
IntArray s1, s2, s3;
3.
4.
sycl::buffer a_buf(a);5.
sycl::buffer b_buf(b);6.
sycl::buffer sum_buf1(s1);7.
sycl::buffer sum_buf2(s2);8.
sycl::buffer sum_buf3(s3);9.
10.
size_t num_groups = 1;
11.
size_t wg_size = 256;12.
auto start = std::chrono::steady_clock::now();13.
for (int i = 0; i < iter; i++) {14.
q.submit([&](sycl::handler &h) {15.
sycl::accessor a_acc(a_buf, h, sycl::read_only);16.
sycl::accessor b_acc(b_buf, h, sycl::read_only);17.
sycl::accessor sum_acc(sum_buf1, h, sycl::write_only, sycl::no_init);
18.
19.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
20.
[=](sycl::nd_item<1> index) {21.
size_t loc_id = index.get_local_id();22.
sum_acc[loc_id] = 0;
23.
for (int j = 0; j < 1000; j++)24.
for (size_t i = loc_id; i < array_size; i += wg_size) {25.
sum_acc[loc_id] += a_acc[i] + b_acc[i];
26.
}
27.
});
28.
});
29.
q.submit([&](sycl::handler &h) {30.
sycl::accessor a_acc(a_buf, h, sycl::read_only);31.
sycl::accessor b_acc(b_buf, h, sycl::read_only);32.
sycl::accessor sum_acc(sum_buf2, h, sycl::write_only, sycl::no_init);
33.
34.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
35.
[=](sycl::nd_item<1> index) {36.
size_t loc_id = index.get_local_id();37.
sum_acc[loc_id] = 0;38.
for (int j = 0; j < 1000; j++)39.
for (size_t i = loc_id; i < array_size; i += wg_size) {96

oneAPI GPU 最適化ガイド40.
sum_acc[loc_id] += a_acc[i] + b_acc[i];
41.
}
42.
});
43.
});
44.
q.submit([&](sycl::handler &h) {45.
sycl::accessor a_acc(a_buf, h, sycl::read_only);46.
sycl::accessor b_acc(b_buf, h, sycl::read_only);47.
sycl::accessor sum_acc(sum_buf3, h, sycl::write_only, sycl::no_init);
48.
49.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
50.
[=](sycl::nd_item<1> index) {51.
size_t loc_id = index.get_local_id();52.
sum_acc[loc_id] = 0;53.
for (int j = 0; j < 1000; j++)54.
for (size_t i = loc_id; i < array_size; i += wg_size) {55.
sum_acc[loc_id] += a_acc[i] + b_acc[i];
56.
}
57.
});
58.
});
59.
}
60.
q.wait();61.
auto end = std::chrono::steady_clock::now();62.
std::cout << "multi_queue completed on device - took "63.
<< (end - start).count() << " u-secs\n";64.
// 結果をチェック
65.
return ((end - start).count());
66. } // multi_queue の終わり

キューがインオーダーである場合、マシンに十分なリソースがあってカーネル間に依存関係がなくても、カーネルを並列に実行することはできず、シーケンシャルに実行されます。これは、すべてのカーネルの合計実行時間が長くなることからも分かります。キューの作成とカーネルの送信方法を以下に示します。
sycl::property_list q_prop{sycl::property::queue::in_order()};std::cout << "In order queue: Jitting+Execution time\n";sycl::queue q1(sycl::default_selector_v, q_prop);
multi_queue(q1, a, b);usleep(500 * 1000);
std::cout << "In order queue: Execution time\n";multi_queue(q1, a, b);

キューがアウトオブオーダーである場合、全体の実行時間はかなり短縮されますが、これは、マシンがキューからカーネルを同時に実行できることを意味します。キューの作成とカーネルの起動方法を以下に示します。
sycl::queue q2(sycl::default_selector_v);
std::cout << "Out of order queue: Jitting+Execution time\n";multi_queue(q2, a, b);usleep(500 * 1000);
std::cout << "Out of order queue: Execution time\n";multi_queue(q2, a, b);

カーネルがあまりスケーリングせず、マシンの計算リソースを効率良く利用できない状況では、work-group/workitem の値を適切に選択して必要とする計算ユニットだけを割り当てて、同時に複数のカーネルを実行することを推奨します。
次のタイムライン・ビューは、インオーダー・キューとアウトオブオーダー・キューで実行されているカーネルを示しています (これは unitrace ツールを使用して収集されました)。ここでは、アウトオブオーダー・キューに送信されたカーネルが並列で実行されていることが明確にわかります。もう 1 つ注目すべき点は、3 つのカーネルすべてが常に並列で実行されるわけではないということです。並列で実行されるカーネルの数は、ハードウェア・リソースの可用性、カー
ネル送信間の時間差など、複数の要因によって左右されます。

97

oneAPI GPU 最適化ガイド
インオーダーとアウトオブオーダー・キューのカーネル実行タイムライン

デバイスクラスの create_sub_devices 関数を使用して、単一のデバイスをサブデバイスに静的に分割することもできます。これにより、プログラマーはカーネルを適切なサブデバイスに送信する際の制御を強化できます。ただし、デバイスのサブデバイスへの分割は静的であるため、ランタイムはカーネルを 1 つのサブデバイスから別のサブデバイ
スに移動する柔軟性がないため、アプリケーションの動的な負荷に適応できません。

5-3-12

複数のキューへカーネルを送信

キューはアクセラレーターで実行するカーネルを送信するチャネルを提供します。また、デバイスのステートを示す
コンテキストも保持します。このステートには、バッファーの内容とカーネルの実行に必要なメモリーが含まれます。
ランタイムは、デバイス・コンテキストを追跡して、ホストとデバイス間の不要なメモリー転送を回避します。そのため、異なるコンテキストでカーネルの送信をインターリーブするのではなく、1 つのコンテキストからカーネルを送信して起動することを推奨します。
次の例では、入力と同じバッファーを使用して計算結果を異なる出力バッファーに生成する 30 個の独立したカーネ
ルを送信します。これらのカーネルは完全に独立しており、同時に実行されるため、順不同となる可能性があります。
カーネルは 3 つのキューに送信されますが、各カーネルの実行にはキューの作成方法に応じて異なるコストが生じます。
1. int VectorAdd(sycl::queue &q1, sycl::queue &q2, sycl::queue &q3,
2.
const IntArray &a, const IntArray &b) {3.
4.
sycl::buffer a_buf(a);5.
sycl::buffer b_buf(b);6.
sycl::buffer<int> *sum_buf[3 * iter];7.
for (size_t i = 0; i < (3 * iter); i++)8.
sum_buf[i] = new sycl::buffer<int>(256);9.
10.
size_t num_groups = 1;
11.
size_t wg_size = 256;12.
auto start = std::chrono::steady_clock::now();13.
for (int i = 0; i < iter; i++) {14.
q1.submit([&](auto &h) {15.
sycl::accessor a_acc(a_buf, h, sycl::read_only);16.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
17.
auto sum_acc = sum_buf[3 * i]->get_access<sycl::access::mode::write>(h);18.
19.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
20.
[=](sycl::nd_item<1> index) {21.
size_t loc_id = index.get_local_id();22.
sum_acc[loc_id] = 0;
23.
for (size_t i = loc_id; i < array_size; i += wg_size) {24.
sum_acc[loc_id] += a_acc[i] + b_acc[i];
25.
}
26.
});
27.
});
28.
q2.submit([&](auto &h) {29.
sycl::accessor a_acc(a_buf, h, sycl::read_only);30.
sycl::accessor b_acc(b_buf, h, sycl::read_only);

98

oneAPI GPU 最適化ガイド
31.
auto sum_acc =32.
sum_buf[3 * i + 1]->get_access<sycl::access::mode::write>(h);33.
34.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
35.
[=](sycl::nd_item<1> index) {36.
size_t loc_id = index.get_local_id();37.
sum_acc[loc_id] = 0;38.
for (size_t i = loc_id; i < array_size; i += wg_size) {39.
sum_acc[loc_id] += a_acc[i] + b_acc[i];
40.
}
41.
});
42.
});
43.
q3.submit([&](auto &h) {44.
sycl::accessor a_acc(a_buf, h, sycl::read_only);45.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
46.
auto sum_acc =47.
sum_buf[3 * i + 2]->get_access<sycl::access::mode::write>(h);48.
49.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
50.
[=](sycl::nd_item<1> index) {51.
size_t loc_id = index.get_local_id();52.
sum_acc[loc_id] = 0;53.
for (size_t i = loc_id; i < array_size; i += wg_size) {54.
sum_acc[loc_id] += a_acc[i] + b_acc[i];
55.
}
56.
});
57.
});
58.
}
59.
q1.wait();60.
q2.wait();61.
q3.wait();62.
auto end = std::chrono::steady_clock::now();63.
std::cout << "Vector add completed on device - took " << (end - start).count()
64.
<< " u-secs\n";65.
// 結果をチェック
66.
for (size_t i = 0; i < (3 * iter); i++)67.
delete sum_buf[i];68.
return ((end - start).count());
69. }// VectorAdd の終わり

カーネルを同一キューに送信すると、カーネルの転送は最初の一度だけ行われるため、転送のオ―バーヘッドが軽減
して最高のパフォーマンスが得られます。
VectorAdd(q, q, q, a, b);

カーネルが同じコンテキストを共有する異なるキューで送信される場合、パフォーマンスは単一のキューで送信された場合とほぼ同等です。ここで注意すべきことは、カーネルが異なるコンテキストで新しいキューに送信されると、JITプロセスはカーネルをコンテキストに関連する新しいデバイス向けにコンパイルすることです。JIT コンパイルの時間を除けば、カーネルの実行時間はほぼ同じです。
sycl::queue q1(sycl::default_selector_v);
sycl::queue q2(q1.get_context(), sycl::default_selector_v);
sycl::queue q3(q1.get_context(), sycl::default_selector_v);
VectorAdd(q1, q2, q3, a, b);

カーネルが異なるコンテキストを持つ 3 つの異なるキューに送信される場合、カーネル呼び出し時にランタイムはすべての入力バッファーをアクセラレーターに毎回転送する必要があるため、パフォーマンスが低下します。さらに、カーネルはコンテキストごとに JIT されます。
sycl::queue q4(sycl::default_selector_v);
sycl::queue q5(sycl::default_selector_v);
sycl::queue q6(sycl::default_selector_v);

99

oneAPI GPU 最適化ガイド
VectorAdd(q4, q5, q6, a, b);

何らかの理由で異なるキューを使用する必要がある場合、共有コンテキストでキューを作成することでこの問題を回避できます。これにより、入力バッファーを転送する必要はなくなりますが、すべての出力バッファーがコンテキスト内に存在するため、カーネルが必要とするメモリー容量が増加します (以前は、デバイス上の同じメモリーを出力バッ
ファーとして使用していました)。忘れてならないことは、カーネルのメモリー転送と計算比率の問題です。上記の例では、カーネルの計算量が少ないため、全体の実行はメモリー転送に依存します。計算量が多い場合、転送は実行全体にそれほど影響しません。
これは、カーネルの計算量が 1000 倍に増加し、実行時間が異なる次の例からも分かります。
1. int VectorAdd(sycl::queue &q1, sycl::queue &q2, sycl::queue &q3,
2.
const IntArray &a, const IntArray &b) {3.
4.
sycl::buffer a_buf(a);5.
sycl::buffer b_buf(b);6.
sycl::buffer<int> *sum_buf[3 * iter];7.
for (size_t i = 0; i < (3 * iter); i++)8.
sum_buf[i] = new sycl::buffer<int>(256);9.
10.
size_t num_groups = 1;
11.
size_t wg_size = 256;12.
auto start = std::chrono::steady_clock::now();13.
for (int i = 0; i < iter; i++) {14.
q1.submit([&](auto &h) {15.
sycl::accessor a_acc(a_buf, h, sycl::read_only);16.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
17.
auto sum_acc = sum_buf[3 * i]->get_access<sycl::access::mode::write>(h);18.
19.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
20.
[=](sycl::nd_item<1> index) {21.
size_t loc_id = index.get_local_id();22.
sum_acc[loc_id] = 0;
23.
for (int j = 0; j < 1000; j++)24.
for (size_t i = loc_id; i < array_size; i += wg_size) {25.
sum_acc[loc_id] += a_acc[i] + b_acc[i];
26.
}
27.
});
28.
});
29.
q2.submit([&](auto &h) {30.
sycl::accessor a_acc(a_buf, h, sycl::read_only);31.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
32.
auto sum_acc =33.
sum_buf[3 * i + 1]->get_access<sycl::access::mode::write>(h);34.
35.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),
36.
[=](sycl::nd_item<1> index) {37.
size_t loc_id = index.get_local_id();38.
sum_acc[loc_id] = 0;39.
for (int j = 0; j < 1000; j++)40.
for (size_t i = loc_id; i < array_size; i += wg_size) {41.
sum_acc[loc_id] += a_acc[i] + b_acc[i];
42.
}
43.
});
44.
});
45.
q3.submit([&](auto &h) {46.
sycl::accessor a_acc(a_buf, h, sycl::read_only);47.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
48.
auto sum_acc =49.
sum_buf[3 * i + 2]->get_access<sycl::access::mode::write>(h);50.
51.
h.parallel_for(sycl::nd_range<1>(num_groups * wg_size, wg_size),52.
[=](sycl::nd_item<1> index) {53.
size_t loc_id = index.get_local_id();54.
sum_acc[loc_id] = 0;100

oneAPI GPU 最適化ガイド
55.
for (int j = 0; j < 1000; j++)56.
for (size_t i = loc_id; i < array_size; i += wg_size) {57.
sum_acc[loc_id] += a_acc[i] + b_acc[i];
58.
}
59.
});
60.
});
61.
}
62.
q1.wait();63.
q2.wait();64.
q3.wait();65.
auto end = std::chrono::steady_clock::now();66.
std::cout << "Vector add completed on device - took " << (end - start).count()
67.
<< " u-secs\n";68.
// 結果をチェック
69.
for (size_t i = 0; i < (3 * iter); i++)70.
delete sum_buf[i];71.
return ((end - start).count());
72. }// VectorAdd の終わり

5-3-13

冗長なキューの構築を避ける

デバイスでカーネルを実行するには、関連するコンテキスト、プラットフォーム、およびデバイスを参照するキューを作成する必要があります。コンテキスト、プラットフォーム、およびデバイスは自動的に選択されるか、ユーザーが指定できます。
コンテキストは、ユーザーが直接構築するか、またはキューの作成時に暗黙的に構築され、SYCL* ランタイムとSYCL* バックエンドがデバイス上の動作に必要なすべてのランタイム情報を保持します。コンテキストが省略されてキューが作成されると、デフォルトのコンストラクターを使用して暗黙的に新しいコンテキストが構築されます。
一般的に、新しいコンテキストを作成すると、カーネルがそのコンテキストでキューに送信されるたびに JIT がプログ
ラムをコンパイルするため重い操作であると言えます。良好なパフォーマンスを得るには、アプリケーションで使用するコンテキストをできるだけ少なくする必要があります。
次の例では、ループ内でキューが作成され、カーネルはその新しいキューに送信されます。これは、ループ反復ごとにJIT コンパイラーが呼び出されます。

101

oneAPI GPU 最適化ガイド
1. int reductionMultipleQMultipleC(std::vector<int> &data, int iter) {2.
const size_t data_size = data.size();3.
int sum = 0;
4.
5.
int work_group_size = 512;6.
int num_work_groups = 1;
7.
int num_work_items = work_group_size;
8.
9.
const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};10.
11.
sycl::buffer<int> buf(data.data(), data_size, props);
12.
sycl::buffer<int> sum_buf(&sum, 1, props);
13.
14.
sycl::queue q1{sycl::default_selector_v, exception_handler};15.
// デバイス上のデータを初期化
16.
q1.submit([&](auto &h) {17.
sycl::accessor buf_acc(buf, h, sycl::write_only, sycl::no_init);
18.
h.parallel_for(data_size, [=](auto index) { buf_acc[index] = 1; });
19.
});
20.
21.
double elapsed = 0;22.
for (int i = 0; i < iter; i++) {23.
sycl::queue q2{sycl::default_selector_v, exception_handler};24.
if (i == 0)25.
std::cout << q2.get_device().get_info<sycl::info::device::name>() << "\n";26.
// reductionMapToHWVector main の始まり
27.
Timer timer;28.
q2.submit([&](auto &h) {29.
sycl::accessor buf_acc(buf, h, sycl::read_only);30.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
31.
sycl::local_accessor<int, 1> scratch(work_group_size, h);32.
h.parallel_for(sycl::nd_range<1>{num_work_items, work_group_size},
33.
[=](sycl::nd_item<1> item) {34.
size_t loc_id = item.get_local_id(0);35.
int sum = 0;36.
for (int i = loc_id; i < data_size; i += num_work_items)
37.
sum += buf_acc[i];38.
scratch[loc_id] = sum;39.
for (int i = work_group_size / 2; i > 0; i >>= 1) {40.
item.barrier(sycl::access::fence_space::local_space);41.
if (loc_id < i)42.
scratch[loc_id] += scratch[loc_id + i];
43.
}
44.
if (loc_id == 0)45.
sum_acc[0] = scratch[0];
46.
});
47.
});
48.
// reductionMapToHWVector main の終わり
49.
q2.wait();50.
sycl::host_accessor h_acc(sum_buf);
51.
sum = h_acc[0];52.
elapsed += timer.Elapsed();
53.
}
54.
elapsed = elapsed / iter;55.
if (sum == sum_expected)
56.
std::cout << "SUCCESS: Time reductionMultipleQMultipleC= " << elapsed
57.
<< "s"
58.
<< " sum = " << sum << "\n";
59.
else
60.
std::cout << "ERROR: reductionMultipleQMultipleC Expected " << sum_expected
61.
<< " but got " << sum << "\n";62.
return sum;63. } // reductionMultipleQMultipleC の終わり

上記のプログラムは、キューの宣言をループ外に移動することで大幅にパフォーマンスを向上できます。

102

oneAPI GPU 最適化ガイド
1. int reductionSingleQ(std::vector<int> &data, int iter) {2.
const size_t data_size = data.size();3.
int sum = 0;
4.
5.
int work_group_size = 512;6.
int num_work_groups = 1;
7.
int num_work_items = work_group_size;
8.
9.
const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};10.
11.
sycl::buffer<int> buf(data.data(), data_size, props);
12.
sycl::buffer<int> sum_buf(&sum, 1, props);
13.
sycl::queue q{sycl::default_selector_v, exception_handler};14.
std::cout << q.get_device().get_info<sycl::info::device::name>() << "\n";15.
16.
// デバイス上のデータを初期化
17.
q.submit([&](auto &h) {18.
sycl::accessor buf_acc(buf, h, sycl::write_only, sycl::no_init);
19.
h.parallel_for(data_size, [=](auto index) { buf_acc[index] = 1; });
20.
});
21.
22.
double elapsed = 0;
23.
for (int i = 0; i < iter; i++) {24.
// reductionIntBarrier main の始まり
25.
Timer timer;
26.
q.submit([&](auto &h) {27.
sycl::accessor buf_acc(buf, h, sycl::read_only);28.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
29.
sycl::local_accessor<int, 1 > scratch(work_group_size, h);30.
h.parallel_for(sycl::nd_range<1>{num_work_items, work_group_size},
31.
[=](sycl::nd_item<1> item) {32.
size_t loc_id = item.get_local_id(0);33.
int sum = 0;34.
for (int i = loc_id; i < data_size; i += num_work_items)
35.
sum += buf_acc[i];36.
scratch[loc_id] = sum;37.
for (int i = work_group_size / 2; i > 0; i >>= 1) {38.
item.barrier(sycl::access::fence_space::local_space);39.
if (loc_id < i)40.
scratch[loc_id] += scratch[loc_id + i];
41.
}
42.
if (loc_id == 0)43.
sum_acc[0] = scratch[0];
44.
});
45.
});
46.
// reductionSingleQ main の終わり
47.
q.wait();48.
sycl::host_accessor h_acc(sum_buf);49.
sum = h_acc[0];50.
elapsed += timer.Elapsed();
51.
}
52.
elapsed = elapsed / iter;53.
if (sum == sum_expected)
54.
std::cout << "SUCCESS: Time reductionSingleQ= " << elapsed << "s"55.
<< " sum = " << sum << "\n";
56.
else
57.
std::cout << "ERROR: reductionSingleQ Expected " << sum_expected
58.
<< " but got " << sum << "\n";59.
return sum;60. } // reductionSingleQ の終わり

複数のキューを作成する場合、キュー間でコンテキストを共有するようにします。これによりパフォーマンスが向上します。上記のカーネルは次のように書き直すことができ、ここではループ内で作成されたキューとループ外のキューがコンテキストを共有しています。この場合、パフォーマンスは単一のキューを使用する場合とほぼ同等です。

103

oneAPI GPU 最適化ガイド
1. int reductionMultipleQSingleC(std::vector<int> &data, int iter) {2.
const size_t data_size = data.size();3.
int sum = 0;
4.
5.
int work_group_size = 512;6.
int num_work_groups = 1;
7.
int num_work_items = work_group_size;
8.
9.
const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};10.
11.
sycl::buffer<int> buf(data.data(), data_size, props);
12.
sycl::buffer<int> sum_buf(&sum, 1, props);
13.
14.
sycl::queue q1{sycl::default_selector_v, exception_handler};15.
// デバイス上のデータを初期化
16.
q1.submit([&](auto &h) {17.
sycl::accessor buf_acc(buf, h, sycl::write_only, sycl::no_init);
18.
h.parallel_for(data_size, [=](auto index) { buf_acc[index] = 1; });
19.
});
20.
21.
double elapsed = 0;22.
for (int i = 0; i < iter; i++) {23.
sycl::queue q2{q1.get_context(), sycl::default_selector_v,
24.
exception_handler};25.
if (i == 0)26.
std::cout << q2.get_device().get_info<sycl::info::device::name>() << "\n";27.
// reductionMultipleQSingleC main の始まり
28.
Timer timer;29.
q2.submit([&](auto &h) {30.
sycl::accessor buf_acc(buf, h, sycl::read_only);31.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
32.
sycl::local_accessor<int, 1> scratch(work_group_size, h);33.
h.parallel_for(sycl::nd_range<1>{num_work_items, work_group_size},
34.
[=](sycl::nd_item<1> item) {35.
size_t loc_id = item.get_local_id(0);36.
int sum = 0;37.
for (int i = loc_id; i < data_size; i += num_work_items)
38.
sum += buf_acc[i];39.
scratch[loc_id] = sum;40.
for (int i = work_group_size / 2; i > 0; i >>= 1) {41.
item.barrier(sycl::access::fence_space::local_space);42.
if (loc_id < i)43.
scratch[loc_id] += scratch[loc_id + i];
44.
}
45.
if (loc_id == 0)46.
sum_acc[0] = scratch[0];
47.
});
48.
});
49.
// reductionMultipleQSingleC main の終わり
50.
q2.wait();51.
sycl::host_accessor h_acc(sum_buf);
52.
sum = h_acc[0];53.
elapsed += timer.Elapsed();
54.
}
55.
elapsed = elapsed / iter;56.
if (sum == sum_expected)
57.
std::cout << "SUCCESS: Time reductionMultipleQSingleContext = " << elapsed
58.
<< "s"
59.
<< " sum = " << sum << "\n";
60.
else
61.
std::cout << "ERROR: reductionMultipleQSingleContext Expected "62.
<< sum_expected << " but got " << sum << "\n";63.
return sum;64. } // reductionMultipleQSingleC の終わり

104

oneAPI GPU 最適化ガイド

5-3-14SYCL* 結合行列拡張を使用したインテル® XMX のプログラミング

結合行列は、行列ハードウェア向けのプログラミングの新しい SYCL* 拡張機能です。これにより、CPU 向けのインテル® アドバンスト・マトリックス・エクステンション (インテル® AMX)、GPU 向けのインテル® Xe マトリクス・エク
ステンション (インテル® XMX)、NVIDIA* Tensor コアなどのターゲットを統合できます。一般に、TensorFlow* などのフレームワークや oneDNN などのライブラリーは、多くの種類の AI ユーザーとアプリケーションにとって有用です。
しかし、独自のニューラル・ネットワーク・アプリケーションを構築するユーザーにとって、これらのフレームワークやライブラリーは、ユーザー独自の最適化を適用できない高レベルな実装であり、ライブラリーのサイズが大きく、負荷も大きくなります。さらに、マシンラーニングの分野では、常に新しい操作が導入され変化しているため、フレームワーク
やライブラリーがタイムリーにその解決策を提供できないこともあります。そのような場合、結合行列は、フレームワークよりも抽象化レベルが高く、パフォーマンス、生産性、そして融合機能を提供するため、単一ソースコードから異なる行列ハードウェアをターゲットにすることで移植性も高めます。
この拡張機能の詳細な仕様は、こちら (英語) で確認できます。
結合行列拡張は、 新しいタイプの joint_matrix 、明示的なメモリー操作の joint_matrix_load およびjoint_matrix_store、行列初期化用の joint_matrix_fill、乗算加算操作を行う joint_matrix_mad、そして要素ごとの操作を行う joint_matrix_apply で構成されます。
次のコードでは、カーネルは 3 つの joint_matrix 行列 (tA、tB、tC) を宣言することで結合行列インターフェイスを利用して、tC += tA * tB 操作を実行します。DPAS (Dot Product and Accumulate Systolic) は、インテル®
XMX で実行される基本操作の名称です。joint_matrix の使用例については、インテルの llvm-test-suite リポジ
トリー (英語) を参照してください。
この例をインテル® XMX で正しく実行するには、GPU としてインテル® XMX ハードウェアが必要です。合成行列の実
装には、エミュレーションやフォールバックの機能は提供されません。
1. size_t NDRangeM = M / TM;2.
size_t NDRangeN = N / TN;3.
sycl::buffer<bfloat16, 2> bufA(A.get_data(), sycl::range<2>(M, K));4.
sycl::buffer<bfloat16, 2> bufB(B.get_data(), sycl::range<2>(K, N));5.
sycl::buffer<float, 2> bufC((float *)C.get_data(), sycl::range<2>(M, N));6.
7.
sycl::queue q;8.
q.submit([&](sycl::handler &cgh) {9.
sycl::accessor accC(bufC, cgh, sycl::read_write);10.
sycl::accessor accA(bufA, cgh, sycl::read_only);11.
sycl::accessor accB(bufB, cgh, sycl::read_only);
12.
cgh.parallel_for(13.
sycl::nd_range<2>({NDRangeM, NDRangeN * SG_SZ}, {1, 1 * SG_SZ}),
14.
[=](sycl::nd_item<2> spmd_item) [[intel::reqd_sub_group_size(SG_SZ)]]
15.
16.
{
17.
// The joint matrix API has to be accessed by all the workitems in a18.
// subgroup these functions will be called once by the subgroup no19.
// code divergence between the workitems
20.
const auto global_idx = spmd_item.get_global_id(0);21.
const auto global_idy = spmd_item.get_global_id(1);22.
const auto sg_startx = global_idx - spmd_item.get_local_id(0);23.
const auto sg_starty = global_idy - spmd_item.get_local_id(1);24.
25.
sycl::sub_group sg = spmd_item.get_sub_group();26.
sycl::ext::oneapi::experimental::matrix::joint_matrix<27.
sycl::sub_group, bfloat16, use::a, TM, TK, layout::row_major>
28.
sub_a;
29.
// For B, we assume B has been already VNNIed.
30.
sycl::ext::oneapi::experimental::matrix::joint_matrix<31.
sycl::sub_group, bfloat16, use::b, TK, TN,

105

oneAPI GPU 最適化ガイド
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.

sycl::ext::intel::experimental::matrix::layout::packed>
sub_b;
sycl::ext::oneapi::experimental::matrix::joint_matrix<
sycl::sub_group, float, use::accumulator, TM, TN>
sub_c;
joint_matrix_fill(sg, sub_c, 1.0);
for (int k = 0; k < K / TK; k += 1) {joint_matrix_load(sg, sub_a,accA.template get_multi_ptr<sycl::access::decorated::no>() +(sg_startx * TM) * K + k * TK,
K);joint_matrix_load(sg, sub_b,
accB.template get_multi_ptr<sycl::access::decorated::no>() +(k * TK / 2) * (N * 2) + sg_starty / SG_SZ * TN * 2,
N * 2);
sub_c = joint_matrix_mad(sg, sub_a, sub_b, sub_c);}
joint_matrix_apply(sg, sub_c, [=](float &x) { x *= ALPHA; });joint_matrix_store(sg, sub_c,
accC.template get_multi_ptr<sycl::access::decorated::no>() +(sg_startx * TM) * N + sg_starty / SG_SZ * TN,N, layout::row_major);}); // parallel for}).wait();

この例では、get_info API を使用して照会できるデバイス・マトリックス記述子を使用して、さまざまな GPU および
CPU 世代でサポートされているさまざまな形状を照会します。
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.

106

std::vector<sycl::ext::oneapi::experimental::matrix::combination>
combinations = q.get_device().get_info<sycl::ext::oneapi::experimental::info::
device::matrix_combinations>();bool passed = true;for (unsigned int i = 0; i < combinations.size(); i++) {if (combinations[i].nsize == 0) { // Intel AMX
passed &=test<int8_t, int8_t, int32_t, 16, 16, 64, class amx_int_16x16x64>();
passed &= test<bfloat16, bfloat16, float, 16, 16, 32,class amx_bf16_16x16x32>();
break;}
if (combinations[i].nsize == 16) { // architecture::intel_gpu_pvc
passed &=test<int8_t, int8_t, int32_t, 8, 16, 32, class pvc_int_8x16x32>();
passed &=test<bfloat16, bfloat16, float, 8, 16, 16, class pvc_bf16_8x16x16>();break;}
if (combinations[i].nsize == 8) { // architecture::intel_gpu_dg2*
passed &= test<int8_t, int8_t, int32_t, 8, 8, 32, class dg2_int_8x8x32>();
passed &=test<bfloat16, bfloat16, float, 8, 8, 16, class dg2_bf16_8x16x16>();break;}}

oneAPI GPU 最適化ガイド
インテル® XMX を使用して最適なパフォーマンスを得るには、インテル® XMX に最大の乗算と操作/サイクルを維持するのに必要なデータを供給し続けるように GEMM カーネルを作成する必要があります。そのチューニング手法の一部を以下に示します。
•

1 つの sub-group が複数の DPAS 操作を実行できます。

•

キャッシュ局所性のブロック化は、3 つの i、j、および k 次元で行う必要があります。最初の 2 つの次元のブロッ
ク化は、カーネルのグローバルレンジに含まれます。k レベルのキャッシュブロック化は、カーネル内で行われます。例えば、256 x 256 x 32 のブロック係数では、グローバルレンジは次のようになります。
range<2> global{M / 256, N / 256 * SG_SIZE};

次に、1 つの sub-group を選択して 64 x 32 x 32 要素を実行すると、ローカルレンジは次のようになります。
range<2> local{256 / 64, 256 / 32 * SG_SIZE};•

スレッドごとにラージ GRF モードを使用すると、GEMM カーネルで最良の結果が得られます。これには、次のコンパイルコマンドを使用します。
$ icpx -fsycl -fsycl-targets=spir64_gen -Xsycl-target-backend "-device pvc -options ze-opt-large-register-file" joint-matrix.cpp

5-3-15

カーネル内での I/O

Print 文は、プログラムの結果を確認する最も基本的な機能です。アクセラレーターでは、プリントはかなり困難であり、オーバーヘッドの面でも高コストです。
SYCL* は、このタスクが標準の I/O C/C++ プログラムと同様になるようにいくつかの機能を提供しますが、アクセラ
レーターの仕組み上、理解しておかなければならない点があります。SYCL* カーネルでは、ファイル I/O は許可されません。
SYCL* は、カーネル内からコンソールに情報を出力する stream クラスを提供します。これにより、デバッガーを使用することなく簡単なデバッグができます。stream クラスは、C++ STL ostream クラスとよく似た機能を提供し、使い方は STL クラスに類似しています。以下では、SYCL* stream クラスを使用して、キューに投入されたカーネルから情報を出力する方法について説明します。
クラスを使用するには、最初にクラスをインスタンス化する必要があります。stream コンストラクターのシグネ
チャーは次のとおりです。
stream(size_t BufferSize, size_t MaxStatementSize, handler &CGH);

コンストラクターには 3 つのパラメーターがあります。
•••

BufferSize: カーネルレンジ全体で出力する文字の総数
MaxStatementSize: stream クラスの 1 回の呼び出しの最大文字数
CGH: sycl::queue::submit 呼び出しの sycl::handler パラメーターへの参照

使用法は、C++ STL ostream std::cout クラスの使い方と類似しています。出力するメッセージまたはデータは、
適切なオペレーター << メソッドにより SYCL* ストリーム・インスタンスに送信されます。SYCL* は、すべてのビルト
イン・データ・タイプ (int、char、float など) と一般的なクラス (sycl::nd_range や sycl::group など) の実装を提供します。

107

oneAPI GPU 最適化ガイド
次に、SYCL stream インスタンスの使い方を示します。
void out1() {constexpr int N = 16;
sycl::queue q;q.submit([&](auto &cgh) {sycl::stream str(8192, 1024, cgh);
cgh.parallel_for(N, [=](sycl::item<1> it) {int id = it[0];/* コンソールに表示するストリームに識別子を送信 */str << "ID=" << id << sycl::endl;});}).wait();} // out1 の終わり

sycl::endl は、C++ STL の std::endl ostream に類似しています。これには、改行を挿入するだけでなく、スト
リームをフラッシュする効果があります。
上記のカーネルをコンパイルして実行すると、次の出力が得られます。
ID=0
ID=1
ID=2
ID=3
ID=4
ID=5
ID=6
ID=7
ID=8
ID=9
ID=10
ID=11
ID=12
ID=13
ID=14
ID=15

BufferSize と MaxStatementSize パラメーターの選択には注意が必要です。サイズが足りない場合、ステートメントが出力されないか、欠落した情報が表示されることがあります。次のカーネルについて考えてみます。
void out2() {sycl::queue q;q.submit([&](auto &cgh) {sycl::stream str(8192, 4, cgh);
cgh.parallel_for(1, [=](sycl::item<1>) {str << "ABC" << sycl::endl; // Print 文 1
str << "ABCDEFG" << sycl::endl; // Print 文 2});}).wait();} // out2 の終わり

上記のカーネルをコンパイルして実行すると、次の出力が得られます。
ABC

表示される文字数が 4 (sycl::endl による改行を含む) であり、MaxStatementSize (sycl::stream の最大ス
テートメント・サイズ・パラメーターで指定) も 4 であるため、最初のステートメントは正しく表示されました。ただし、2番目のステートメントの改行のみが出力されます。

108

oneAPI GPU 最適化ガイド
次のカーネルは、許容される最大文字サイズを増やした場合の影響を示しています。
void out3() {sycl::queue q;q.submit([&](auto &cgh) {sycl::stream str(8192, 10, cgh);
cgh.parallel_for(1, [=](sycl::item<1>) {str << "ABC" << sycl::endl; // Print 文 1
str << "ABCDEFG" << sycl::endl; // Print 文 2});}).wait();} // out3 の終わり

上記のカーネルをコンパイルして実行すると、期待する出力が得られます。
ABC
ABCDEFG

この例では、単一の work-item を持つ単純なカーネルを使用しています。より現実に近いカーネルには、通常、複数の work-item が含まれます。その場合、コンソールに出力されるステートメントの順番に保証はありません。さまざまな work-item のステートメントが順不同で出力されることを理解している必要があります。次のカーネルについて考えてみます。
void out4() {sycl::queue q;q.submit([&](auto &cgh) {sycl::stream str(8192, 1024, cgh);
cgh.parallel_for(sycl::nd_range<1>(32, 4), [=](sycl::nd_item<1> it) {int id = it.get_global_id();str << "ID=" << id << sycl::endl;});}).wait(); // out4 の終わり

実行すると次のような結果が得られます。
ID=0
ID=1
ID=2
ID=3
ID=4
ID=5
[snip]
ID=26
ID=27
ID=28
ID=29
ID=30
ID=31

このプログラムを再度実行すると、スレッドが実行される順序に応じて、全く異なる出力が生成される可能性があり
ます。
ID=4
ID=5
ID=6
ID=7
ID=0
ID=1
[snip]
ID=14

109

oneAPI GPU 最適化ガイド
ID=15
ID=28
ID=29
ID=30
ID=31

sycl::stream からの出力は、カーネルが実行を完了してから出力されます。通常これは問題とはなりません。ただし、カーネルに障害が発生したり、例外がスローされる場合、ステートメントは表示されません。例外が発生する次のカーネルの例を考えてみます。
void out5() {int *m = NULL;
sycl::queue q;q.submit([&](auto &cgh) {sycl::stream str(8192, 1024, cgh);
cgh.parallel_for(sycl::nd_range<1>(32, 4), [=](sycl::nd_item<1> it) {int id = it.get_global_id();str << "ID=" << id << sycl::endl;
if (id == 31)*m = id;});}).wait();} // out5 の終わり

上記のコードをコンパイルして実行すると、NULL ポインターへの書き込みが原因でセグメンテーション違反が発生します。
Segmentation fault (core dumped)

いずれの print 文もコンソールに出力されません。代わりに、セグメンテーション違反を示すエラーメッセージが表示されます。これは、従来の C/C++ ストリームとは異なります。

5-3-16

明示的な SIMD カーネルを最適化

明示的な SIMD SYCL* 拡張 (Explicit SIMD SYCL Extension - ESIMD) を使用すると、カーネル開発者は効率的なメタルに近いプログラミングが可能になり、最適なコード品質を実現してインテル® GPU で最高のパフォーマンスを実現できます。複数の作業項目をサブグループにパックするためにコンパイラーの暗黙的なベクトル化に依存する標準 SYCL* とは異なり、ESIMD は、SIMD 計算の明示的なデータ並列性を表現する直感的な高レベル・インターフェイスを提供します。このセクションでは、効率的な ESIMD コードを記述するためのガイドラインを示します。

5-3-16-1

レジスター利用率の制御

Xe GPU アーキテクチャーでは、汎用レジスター (GRF) がベクトルエンジン上で最速のデータアクセスを提供します。
変数を保持するのに十分なレジスターがない場合、変数をスクラッチメモリーにスピルする必要があり、その結果、メモリーアクセスの遅延が大きくなり、パフォーマンスが大幅に低下することがよくあります。カーネルのパフォーマン
スを向上させ、メモリー・トラフィックを削減するには、GRF リソースを効率的に利用することが重要です。
ESIMD 拡張の中核は、プログラミング・モデルの基礎となる simd ベクトル型です。simd ベクトル型の変数は、コンパイラーによってレジスターに割り当てられます。Xe GPU の各ベクトルエンジンには、ハードウェア・スレッドごとに固定数のレジスターがあります。限られたレジスター空間を効率的に使用するには、GRF の構成と simd 変数のレイアウト規則を理解することが重要です。たとえば、インテル® データセンターGPU マックス・シリーズでは、各ハード
ウェア・スレッドに 64 バイト幅のレジスターが 128 個あります (次の図を参照)。

110

oneAPI GPU 最適化ガイド

simd 型のベクトル変数は、行優先順に GRF に連続的に割り当てられます。標準 SYCL* では、ローカル変数は SIMT
レーンごとに定義され、それが均一かどうかはコンパイラーによって決定されます。ESIMD では、変数はデフォルトで均一であり、柔軟なサイズにすることができます。これにより、カーネル開発者は、さまざまなプログラムの場所でカス
タマイズされたベクトルサイズを使用して、レジスターの負荷と計算スループットの間のより良いトレードオフを実現できます。
変数のサイズが 1 つの GRF を超える場合、次の隣接するレジスターにラップアラウンドされます。変数のサイズがGRF の幅より小さい場合、コンパイラーは複数の変数を 1 つの GRF にパックして断片化を減らすことができます。たとえば、次の例では、ソースオペランドは half float 型の simd ベクトルとして定義されています。したがって、各ベク
トルには 16 バイトが含まれます。
1.
2.
3.
4.

simd<half, 8> va;
simd<half, 8> vb;…
simd<half, 8> vc = va + vb;

この場合、コンパイラーは va と vb の両方を 1 つの GRF にパックし、次の命令を生成できます。
1.

add (8|M0)

r4.0<1>:hf

r3.0<8;8,1>:hf

r3.8<8;8,1>:hf

ESIMD で提供される select 関数を利用することで、プログラマーはサブベクトル演算をさらに制御できます。
select 関数は、サイズとストライドのテンプレート・パラメーターで指定された元の SIMD ベクトル・オブジェクト内の要素のサブセットへの参照を返します。
たとえば、整数値の 32 ビットのプレフィクスの合計を計算するには、入力ベクトルを小さなブロックに分割し、SIMD
計算を実行して各サブブロックの先行するすべての要素の合計を更新し、結果を徐々に大きなブロックに伝播します。
入力ビット値がサイズ 32 の SIMD ベクトル v に格納されているとします。
1.

simd<unsigned short, 32> v;

これは、以下に示すように、インテル® データセンターGPU マックス・シリーズ上の 1 つのレジスターに割り当てられます。

最初のステップは、上の図の赤枠のボックスで示されているように、それぞれ 2 つの隣接する要素を含む 16 個のサ
ブブロックのプレフィクス合計を並列に計算することです。これは、次のように ESIMD コードで簡単に記述できます。
111

oneAPI GPU 最適化ガイド
1.

v.select<16, 2>(1) = v.select<16, 2>(1) + v.select<16, 2>(0);コンパイラーは、偶数位置の要素 (青でハイライト) のビット値を奇数位置の要素 (黄色でハイライト) に追加する 1 つの SIMD 16 命令を生成します。レジスター領域の構文 r3.1<2;1,0>:uw は、要素のデータタイプが符号なしワー
ド、ストライドが 2、開始オフセットが r3.1 であることを意味します。
1.

add (16|M0)

r3.1<2>:uw

r3.1<2;1,0>:uw

r3.0<2;1,0>:uw

レジスターの更新はインプレースで行われることに注意してください。計算のために追加の命令や一時レジスターは生成されません。このステップの後に生成された結果を以下に示します。

3 番目のステップでは、8 つの要素を持つサブブロックのプレフィクス合計の計算を完了する必要があります。各サブブロックでは、下図に示すように、4 番目の要素 (青でハイライト) のプレフィクス合計を次の 4 つの要素 (黄色でハイライト) に累積する必要があります。

これを実装する 1 つの方法は、ブロックごとにベクトルの加算を個別に記述することです。
1.
2.
3.

4.

v.select<4, 1>(4) = v.select<4, 1>(4) + v[3];v.select<4, 1>(12) = v.select<4, 1>(12) + v[11];v.select<4, 1>(20) = v.select<4, 1>(20) + v[19];v.select<4, 1>(28) = v.select<4, 1>(28) + v[27];コンパイラーは、このコードシーケンスに対して SIMD4 加算命令を生成します。ここで、レジスター領域構文
r3.3<0;1,0>:uw は、r3.3 の符号なしワードタイプのスカラー要素を取得し、それをベクトル加算用にブロードキャストすることを意味します。レジスター領域構文 r3.4<4;4,1>:w は、r3.4 から 1 のストライドで 4 つのワー
ドタイプ要素 (r3.4、r3.5、r3.6、r3.7) を取得することを意味します。
1.
2.
3.

4.

add (4|M0)
add (4|M0)
add (4|M0)
add (4|M0)

r3.4<1>:uw
r3.12<1>:uw
r3.16<1>:uw
r3.24<1>:uw

r3.3<0;1,0>:uw
r3.11<0;1,0>:uw
r3.15<0;1,0>:uw
r3.23<0;1,0>:uw

r3.4<4;4,1>:uw
r3.12<4;4,1>:uw
r3.16<4;4,1>:uw
r3.24<4;4,1>:uw

この実装は機能しますが、最も効率的ではありません。ESIMD で提供される他のベクトル操作関数を利用することで最適化できます。具体的には、replicate 関数はより柔軟なベクトル選択パターンをサポートし、プログラマーがXe GPU ISA が 提 供 す る 強 力 な レ ジ ス タ ー 領 域 分 割 機 能 を 最 大 限 に 活 用 で き る よ う に し ま す 。 一 方 、
bit_cast_view 関数を使用すると、プログラマーは simd ベクトルを、異なる要素データ型と形状を持つ別の
simd オブジェクトとして再解釈できます。これらの関数の定義は、ESIMD 拡張仕様と API ドキュメントに記載されています。これらの機能を使用すると、上記のコードは次のように書き直すことができます。
1.
2.
3.

4.

112

1: auto t1 = v.replicate_vs_w_hs<4, 8, 4, 0>(3);2: auto t2 = t1.bit_cast_view<unsigned long long>();3: auto t3 = v.bit_cast_view<unsigned long long>();4: t3.select<4, 2>(1) = t3.select<4, 2>(1) + t2;

oneAPI GPU 最適化ガイド
1 行目では、青でハイライトされた各要素を 4 回複製して、16 整数ベクトル「t1」を作成します。2 行目では、「t1」を
4 qword 型ベクトル「t2」として再解釈します。3 行目では、ベクトル「v」を 16 qword 型ベクトル「t3」として再解釈します。最後の行では、「t2」と「t3」から選択された要素に対して SIMD4 加算を実行します。これは、前の図で黄色にハイライトされた要素に対応します。コンパイラーは、ネイティブ qword 加算とリージョン分割をサポートするプラットフォームで、次の最適化されたコードを生成できます。
1.
2.

mov (16|M0)
add (4|M0)

r5.0<1>:uw
r3.1<2>:qr3.3<8;4,0>:uw
r3.1<2;1,0>:qr5.0<4;4,1>:q

一般的に、プログラマーはレジスター割り当てをコンパイラーに任せます。ただし、パフォーマンス・チューニングのためレジスター割り当てを直接制御したい場合は、ESIMD のプライベート・グローバル変数でサポートされている物理
レジスター・バインディングを指定することでこれを実現できます。これは、次の例に示すように、バンク競合の削減な
ど、他のパフォーマンス最適化にも役立ちます。
1.
2.
3.
4.
5.
6.

ESIMD_PRIVATE ESIMD_REGISTER(640) simd<float, 16> va;
ESIMD_PRIVATE ESIMD_REGISTER(960) simd<float, 16> vb;
ESIMD_PRIVATE ESIMD_REGISTER(4096) simd<float, 16> vc;
ESIMD_PRIVATE ESIMD_REGISTER(2560) simd<float, 16> vd;…
vd = vc + va * vb;

ESIMD_REGISTER(n) 属性で指定された数値 n は、simd ベクトルに割り当てられる GRF ブロックの開始バイト オ
フセットに対応します。この場合、コンパイラーは、バンクの競合がないように、プログラマーによって指定された異なるレジスターバンク/バンドルに属する 3 つのソースオペランドを持つ mad 命令を生成します。
1.

mad (16|M0)

5-3-16-2

r40.0<1>:fr64.0<8;1>:fr15.0<8;1>:fr10.0<1>:f

SIMD 幅の制御

標準 SYCL* と ESIMD の重要な違いの 1 つは、固定サブグループ・サイズが 8、16、または 32 であるのに対し、
ESIMD カーネルでは論理サブグループ・サイズが 1 でなければならないことです。つまり、これはベクトルエンジン(VE) 上の単一のハードウェア・スレッドにマップされます。ESIMD カーネル用に生成されたアセンブリー・コードには、
ターゲット GPU デバイスで提供される命令セット・アーキテクチャー (ISA) のサポートに応じて、さまざまな SIMD 幅の命令が含まれる場合があります。
ベクトルサイズが 48 の浮動小数点数であるベクトル加算を実行する例を以下に示します。
1.
2.
3.
4.

simd<float, 48> va;
simd<float, 48> vb;…
simd<float, 48> vc = va + vb;

合法化後、コンパイラーは次のコードを生成し、レジスター r13、r14、r16 とレジスター r17、r18、r11 に含まれる 48組の浮動小数点入力値の合計を計算し、出力値をレジスター r17、r18、r19 に書き込みます。
1.
2.

add (32|M0)
add (16|M0)

r17.0<1>:f
r19.0<1>:f

r17.0<1;1,0>:fr11.0<1;1,0>:fr13.0<1;1,0>:f
r16.0<1;1,0>:f

SIMD ベクトルサイズを変更することで、プログラマーは各 ESIMD スレッドで実行される異なる計算ワークロードを割り当てることができます。これにより、標準 SYCL* でサポートされている固定の SIMD8/16/32 サブグループ・サイ
ズを超える柔軟性が提供され、さまざまなアプリケーションのニーズに応じてデータのパーティション分割と GPU ス
レッドの占有を制御できます。

113

oneAPI GPU 最適化ガイド
最適なコード品質を実現するため、ESIMD カーネル開発者は、ターゲット・アーキテクチャーで許可される合法的なSIMD 命令実行サイズに注意する必要があります。一般に、Xe GPU ISA は、データタイプと命令領域の制限に応じて、SIMD1/2/4/8/16/32 のさまざまな実行サイズをサポートします。カーネルが奇数の SIMD ベクトルサイズで記述されている場合、合法化後にコンパイラーによって余分な命令が生成され、パフォーマンスが低下する可能性があります。たとえば、以下の修正されたベクトル加算の例では、SIMD ベクトルサイズが 47 に変更されています。
1.
2.
3.
4.

simd<float, 47> va;
simd<float, 47> vb;…
simd<float, 47> vc = va + vb;

コンパイラーはこれを SIMD32/8/4/2/1 命令に分割します。
1.
2.
3.
4.
5.

add (32|M0) r32.0<1>:f
add (8|M0) r38.0<1>:f
add (4|M0) r38.8<1>:f
add (2|M0) r38.12<1>:f
add (1|M0) r38.14<1>:f

r32.0<1;1,0>:fr22.0<1;1,0>:f
r22.8<1;1,0>:f
r22.12<1;1,0>:f
r22.14<0;1,0>:fr27.0<1;1,0>:fr31.0<1;1,0>:f
r31.8<1;1,0>:f
r31.12<1;1,0>:f
r31.14<0;1,0>:f

このような場合、命令数のオーバーヘッドを削減するため、次のアライメントされた SIMD 命令の幅に合わせてベクト
ルサイズを増やすことを推奨します。
ベクトルサイズは ESIMD カーネルで明示的に定義されているため、高度なコンパイラー最適化を必要とせず、ベクトル化して期待される SIMD コードを生成します。ただし、simd ベクトルサイズが小さすぎると、生成されたコードは、より広い SIMD 命令をサポートするさまざまな GPU アーキテクチャー間で適切に動作しない可能性があります。たとえば、以下の修正されたベクトル加算の例では、simd ベクトルサイズは 8 に削減されています。
1.
2.
3.
4.

simd<float, 8> va;
simd<float, 8> vb;…
simd<float, 8> vc = va + vb;

コンパイラーは SIMD 8 命令を生成します。
インテル® データセンター GPU マックス・シリーズは 16 個の浮動小数点演算/EU/clk をサポートしているため、
SIMD8 命令ではピーク・スループットの半分しか達成できません。パフォーマンスの移植性を高めるには、可能な場
合はより大きなベクトルサイズを使用し、さまざまなアーキテクチャーの機能に基づいてコンパイラーが最適な命令幅を選択できるようにすることを推奨します。

5-3-16-3

レーン間のデータ共有

メタルに近いレジスター利用率の制御により、ESIMD カーネルは追加コストなしでレーン間のデータ共有を効率的に行うことができます。この利点は、次に示す線形フィルターの例で説明できます。線形フィルターカーネルへの入力
は 2D 画像です。入力画像の各ピクセル (i, j) について、カーネルは 3x3 境界ボックス内のすべての隣接ピクセルの平均値を計算します。

114

oneAPI GPU 最適化ガイド

単純な SYCL* 実装では、各 SIMT スレッドは、境界ボックス内のすべての隣接ピクセルを読み取り、それらの平均値を計算し、結果を書き戻すことによって 1 つのピクセルを処理します。ただし、大量の冗長メモリーのロード/ストアと、共通の隣接ピクセルの関連アドレス計算のため、これは非常に非効率的です。大きなデータブロックを GRF にロードし、レジスターに格納されている隣接ピクセル値を再利用することで、次のように線形フィルターを ESIMD で効率的に実装できます。
1. simd<unsigned char, 8 * 32> vin;
2. auto in = vin.bit_cast_view<unsigned char, 8, 32>();3.
4. simd<unsigned char, 6 * 24> vout;
5. auto out = vout.bit_cast_view<uchar, 6, 24>();6.
7. simd<float, 6 * 24> vm;
8. auto m = vm.bit_cast_view<float, 6, 24>();9.
10. uint h_pos = it.get_id(0);11. uint v_pos = it.get_id(1);12.
13. in = media_block_load<unsigned char, 8, 32>(accInput, h_pos * 24, v_pos * 6);
14.
15. m = in.select<6, 1, 24, 1>(1, 3);
16. m += in.select<6, 1, 24, 1>(0, 0);
17. m += in.select<6, 1, 24, 1>(0, 3);
18. m += in.select<6, 1, 24, 1>(0, 6);
19. m += in.select<6, 1, 24, 1>(1, 0);
20. m += in.select<6, 1, 24, 1>(1, 6);
21. m += in.select<6, 1, 24, 1>(2, 0);
22. m += in.select<6, 1, 24, 1>(2, 3);
23. m += in.select<6, 1, 24, 1>(2, 6);
24. m = m * 0.111f;
25.
26. vout = convert<unsigned char>(vm);

ESIMD カーネルの各スレッドは、8x32 バイトのマトリックスを読み取り、6x8 ピクセルに対応する 6x24 バイトのマ
トリックスを出力します。8x10 入力ピクセルには 8x30 バイトしか必要ありませんが、各行に 2 バイトのパディングを追加すると、計算用のレジスターファイルのレイアウトが適切に整います。選択操作は次のように動作します。入力
ピクセルが 8x32 バイトのマトリックスにロードされた後、各ステップで選択操作によって 6x24 バイトのサブマト
リックスを抽出し、すべての要素を float に変換してから、6x24 浮動小数点マトリックス「m」である実行合計に追加します。実装は基本的に、以下の図に示すように、スライディング・ウィンドウ平均計算を実行します。破線の境界線を持つ各四角形は、m の中間結果を累積するために選択されたサブブロックを表します。メディアブロックのロードおよびストア関数は、それぞれ入力画像から 8x32 2D ブロックを読み取り、出力画像に 6x24 2D ブロックを書き込むために使用され、SIMT の収集/分散関数よりも効率的です。

115

oneAPI GPU 最適化ガイド

5-3-16-4

間接レジスターアクセス

定数ではなく変数インデックスを使用して SIMD ベクトル選択が行われると、コンパイラーは間接レジスターアクセスを使用する命令を生成します。間接レジスターアクセスは、インテル® Xe GPU でサポートされている独自の機能であり、メモリーからの収集/分散よりも高速です。ただし、直接レジスターアクセスと比較すると、複数のレジスターにまたがるサブレジスターをフェッチする余分なサイクルが必要になるため、レイテンシーがさらに高くなる可能性があります。
間接レジスターアクセスを使用してヒストグラムを計算する例を以下に示します。コード例には、入力バイトピクセル
値のヒストグラムを計算するためのネストしたループが含まれています。各外側の反復では 8x32 バイトが処理され、これらは SIMD ベクトル in にロードされ、結果は SIMD ベクトル・ヒストグラムに蓄積されます。in ベクトルとヒスト
グラム・ベクトルの両方がレジスターに割り当てられるため、共有メモリーを経由する実装に比べて効率的です。
1. simd<unsigned char, 8 * 32> in;
2. simd<unsigned int, 256> histogram(0);3. …
4. for (int y = 0; y < 8; y++) {5.
in = media_block_load<unsigned char, 8, 32>(readAcc, h_pos, v_pos);6.
7. for (int i = 0; i < 8; i++) {8.
for (int j = 0; j < 32; j++) {9.
histogram.select<1, 1>(in[i * 32 + j]) += 1;
10.
}
11. }
12. v_pos += 8;
13. }最も内側のループの反復で処理される入力ピクセルのインデックスは、インダクション変数 i と j に依存します。さらに、更新されるヒストグラム・バケットのインデックスは、実際の入力ピクセル値に依存します。デフォルトでは、コンパイラーが最も内側のループをアンロールしない場合、生成されるコードには複数の間接レジスターアクセス命令が含まれます。
BB_3:
add (1|M0) a0.0<1>:uw
r21.4<0;1,0>:uw 0x1B40:uw
mov (1|M0) r1.0<1>:w
r[a0.0]<0;1,0>:ubshl (1|M0) r2.0<1>:w
r1.0<0;1,0>:w
2:w
add (1|M0) a0.0<1>:uw
r2.0<0;1,0>:uw
0x1740:uw
add (1|M0) r[a0.0]<1>:d r[a0.0]<0;1,0>:d 1:w
add (1|M0) r21.2<1>:dr21.2<0;1,0>:d
1:w
cmp (1|M0) (eq)f0.0
null<1>:d r21.2<0;1,0>:d 32:w
(W&~f0.0) jmpi BB_3

116

oneAPI GPU 最適化ガイド
コードの品質を高めるため、プログラマーはループの先頭に「#pragma unroll」ディレクティブを追加して、コンパイ
ラーにループを完全にアンロールするように指示できます。ただし、コンパイル時にループの境界が不明な場合など、常に可能であるとは限らないことに注意してください。ヒストグラムの例では、「#pragma unroll」ディレクティブを追加することで、内部ループを完全にアンロールできます。
simd<unsigned char, 8 * 32> in;simd<unsigned int, 256> histogram(0);…
for (int y = 0; y < 8; y++) {in = media_block_load<unsigned char, 8, 32>(readAcc, h_pos, v_pos);#pragma unroll
for (int i = 0; i < 8; i++) {#pragma unroll
for (int j = 0; j < 32; j++) {histogram.select<1, 1>(in[i * 32 + j]) += 1;}}
v_pos += 8;}

アンロール後、ベクトル「in」の動的インデックスはコンパイル時に推測でき、結果のコードは直接レジスターアクセ
スを行う命令に変換できます。コンパイラーは、アドレス計算コードを最適化し、ヒストグラム更新の後続コードで使用される 16 個のアドレス値を更新するため 1 つの SIMD16 命令を生成することもできます。インデックス値はメモリーに依存し、コンパイル時に決定できないため、ヒストグラム・ベクトルへのアクセスは間接レジスターアクセスのままであることに注意してください。

5-3-16-5

条件付き操作ESIMD は、「if/else」や「for」ループ・ステートメントなどの標準 C++ 制御フロー構造をサポートしています。ただし、このようなスカラー制御フロー構造を使用してプログラミングすると、分岐命令のオーバーヘッドにより、コードが非効率になり、パフォーマンスが低下する可能性があります。次の例では、ベクトル vc のデータ要素は、ベクトル va とvb を含む条件式に応じて異なる値に割り当てられます。
1. simd<int, 16> va;
2. simd<int, 16> vb;
3. simd<int, 16> vc;
4. …
5. for (int i = 0; i < 16; i++) {6. if ((va[i] > 0) && (vb[i] > 0))7.
vc[i] = x;
8. else9.
vc[i] = y;10. }ループアンロールを使用しない場合、コンパイラーは分岐命令を含むコードを生成します。完全なループアンロールを有効にすると、前述のように間接レジスターアクセスを排除できますが、各 vc 要素を更新するため複数の SIMD1
命令が生成されます。
1. BB_1:
2. shl (1|M0) r2.2<1>:w
r2.0<0;1,0>:w
2:w
3. add (1|M0) a0.2<1>:uw r2.2<0;1,0>:uw 0xC0:uw
4. add (1|M0) a0.1<1>:uw r2.2<0;1,0>:uw 0xA0:uw
5. add (1|M0) a0.0<1>:uw r2.2<0;1,0>:uw 0x80:uw
6. cmp (1|M0) (lt)f1.0
null<1>:d r[a0.1]<0;1,0>:d 1:w
7. (~f1.0) cmp (1|M0) (lt)f1.0 null<1>:d r[a0.0]<0;1,0>:d 1:w
8. (f1.0) sel (1|M0) r[a0.2]<1>:d r2.3<0;1,0>:d r2.2<0;1,0>:d
9. add (1|M0) r2.0<1>:d r2.0<0;1,0>:d 1:w
10. cmp (1|M0) (eq)f0.1
null<1>:d r2.0<0;1,0>:d 16:w
11. (W)and (1|M0) f0.0<1>:uw f0.1<0;1,0>:uw 0x1:uw

117

oneAPI GPU 最適化ガイド
12. (~f0.0.any16h) goto.b (16|M0)_L_k0_0_

BB_1

ESIMD は、simd ベクトルの一般的な比較演算子と論理演算子をサポートします。結果は simd_mask 変数に格納されます。simd_mask を入力パラメーターとして受け取る条件付きマージ関数を使用すると、上記のコードは次のよ
うに書き直すことができます。
1.
2.

simd_mask<16> vmask = (va > 0) & (vb > 0);
vc.merge(0, 1, vmask);

この変更により、コンパイラーは SIMD16 命令を生成し、va 要素と vb 要素を比較して simd_mask を生成します。
これは、条件付き割り当てを実行するため後続のプレディケート選択命令で使用されます。
1.
2.
3.
4.

cmp (16|M0) (gt)f0.1 null<1>:d r5.0<8;8,1>:d 0:w(f0.1)sel (16|M0) r7.0<1>:d r2.2<0;1,0>:d r2.3<0;1,0>:dcmp (16|M0) (gt)f0.0 null<1>:d r4.0<8;8,1>:d 0:w(f0.0)sel (16|M0) r6.0<1>:d r7.0<8;8,1>:d r2.3<0;1,0>:d

さらに、ESIMD は、simd_mask タイプの変数に対して 2 種類のブール減算演算をサポートしています。「any」減算
は、マスク要素のいずれかが true の場合に true を返しますが、「all」減算は、すべてのマスク要素が true の場合にのみ true を返します。ブール減算は、パフォーマンスの最適化に役立つ機能です。次のコードは、色調整を実行す
るカーネルの例を示しています。各ピクセルについて、R/G/B チャネルの順序を確認し、それに応じて色相を調整します。
1. simd<int, 8> hue, tmp;
2. simd_mask<8> vmask;
3. simd<unsigned short, 8> maxChannel, minChannel;
4. ...
5. // R>B>G
6. vmask = ((maxChannel == RED) & (minChannel == GREEN));
7. tmp = adjustHue(436906);
8. hue.merge(tmp, vmask);
9. // B>R>G
10. vmask = ((maxChannel == BLUE) & (minChannel == GREEN));
11. tmp = adjustHue(349525);
12. hue.merge(tmp, vmask);

simd_mask の評価には共通条件が含まれるため、コードをリファクタリングして次のように書き直すことができます。
1. simd<int, 8> hue, tmp;
2. simd_mask<8> vmask;
3. simd<unsigned short, 8> maxChannel, minChannel;
4. ...
5. vmask = (minChannel == GREEN);
6. if (vmask.any()) {7.
// R>B>G8.
tmp = adjustHue(436906);
9.
hue.merge(tmp, (maxChannel == RED) & vmask);
10.
//B>R>G11.
tmp = adjustHue(349525);
12.
hue.merge(tmp, (maxChannel == BLUE) & vmask);
13. }このアプローチの利点は、入力データに最小限の GREEN チャンネルのピクセルがほとんど含まれていない場合、
vmask.any() はほとんどのスレッドに対して false と評価され、早期に終了できるため、パフォーマンスが向上し、電力が節約されることです。

118

oneAPI GPU 最適化ガイド

5-3-16-6

数学関数

計算集約型アプリケーションでは、適用可能であれば高速な数学命令を利用することで、パフォーマンスを大幅に向上できます。Xe GPU アーキテクチャーは、一般的な数学命令に対するネイティブ・ハードウェアのサポートを提供します。コード生成時に、コンパイラーはパターンマッチングによりネイティブの数学命令を生成することができますが、常に保証されるわけではありません。プログラマーが特定のコードに対して呼び出すネイティブ数学組み込み関数を明示的に制御する必要がある場合は、ESIMD で提供される数学関数 API を使用できます。このような数学関数の精度は、ハードウェア実装の精度に制限され、標準の SYCL* 仕様と一致しない可能性があることに注意してください。
結果の精度要件に一致する適切な組み込み関数を選択するかどうかは、プログラマーの裁量に委ねられます。
たとえば、3 つの変数に対して任意の組み合わせのブール演算を実行するには、ESIMD プログラマーは次のカーネ
ルに示すように bfn API を使用できます。
1.
2.
3.
4.
5.
6.
7.
8.

constexpr bfn_t F = ~bfn_t::x | bfn_t::y ^ bfn_t::z;…
simd<unsigned int, 16> va;
simd<unsigned int, 16> vb;
simd<unsigned int, 16> vc;
simd<unsigned int, 16> vd;…
vd = bfn<F>(va, vb, vc);

これは、個別のブール論理命令の代わりに、ブール式「~s0|s1^s2」を実行する単一の 3 ソース bfn 命令にマッピン
グされます。
1.

bfn.(~s0|s1^s2) (16|M0) r4.0<1>:ud r1.0<1;0>:ud r3.0<1;0>:ud r2.0<1>:ud

もう 1 つの便利な機能は、リダクション関数です。指定された simd オブジェクトに対して、コンパイラーはターゲット
デバイス上で最適なコードシーケンスを生成し、指定された操作をすべてのスカラー要素に適用します。要素ごとの操作の順序は保証されず、結果の正確さは特定の計算順序に依存しないことに注意してください。simd ベクトル内のすべてのスカラー要素の合計を計算する例を以下に示します。
1.
2.
3.
4.

simd<unsigned int, 64> va;
int sum;…
sum = reduce<int>(va, std::plus<>());

ESIMD で利用可能な数学 API の完全なリストについては、以下を参照してください:
DPC++ ランタイム: ESIMD 数学演算。(intel.github.io)

5-3-16-7

メモリー関数ESIMD は、さまざまなメモリーアクセス機能をサポートする豊富な API セットを提供します。これらの API の一部は高レベルで定義されており、一般的な GPU デバイスターゲットに対してより移植性の高いインターフェイスを備えています。たとえば、次のコードは、copy_from() / copy_to() メソッドを使用して、メモリーから simd オブジェクトにデータを読み取ったり、値をメモリーに書き込んだりする方法を示しています。この場合、コンパイラーはアクセスパターンとランタイムターゲットに基づいてメモリー・アクセス・メッセージ・タイプを決定します。
1.
2.
3.
4.

// Src and Dst are USM pointers defined in the host code
simd<unsigned int, 32> va;
va.copy_from(Src + offset);va.copy_to(Dst + offset);119

oneAPI GPU 最適化ガイド
その他の API はより低レベルで定義されており、基盤となる Xe GPU メモリーシステム操作によく似ています。これにより、プログラマーはパフォーマンス・チューニングの目的でメッセージタイプと属性を完全に制御できます。最も便利な機能の 1 つは、ブロックロード/ストア機能です。入力次元に応じて、ESIMD は 1D と 2D の両方のブロックメッ
セージをサポートします。たとえば、次のコードは、グローバルメモリーとの間で 2D ブロックロード/ストアを実行する方法を示しています。
1.
2.
3.
4.
5.
6.
7.
8.
9.

1:
2:
3:
4:
5:
6:
7:

constexpr uint32_t width = SurfaceWidth * sizeof(float) - 1;
constexpr uint32_t height = SurfaceHeight - 1;
constexpr uint32_t pitch = SurfacePitch * sizeof(float) - 1;
config_2d_mem_access<int, BlockWidth, BlockHeight, NumBlocks>
payload(input, width, height, pitch, x, y);auto data = lsc_load_2d(payload);…
payload.set_data_pointer(output);
lsc_store_2d(payload, data);

1 ～ 3 行目では、メモリーに格納されている 2D ブロックの幅、高さ、ピッチを計算します。4 行目は、ブロックの幅、高さ、ピッチ、および x 座標と y 座標を含む 2D ブロック・メッセージ・ペイロードを作成するのに使用される便利な APIです。これをリファクタリングする利点は、幅やピッチなどの一部のフィールドはほとんど変更されず、複数のメッセー
ジ間で共有できるため、冗長なペイロードフィールド設定を回避できることです。5 行目では、2D ブロックロードを実行し、返されたデータを後続のベクトルエンジン計算のために GRF に保存します。6 行目では、ペイロード設定の出力メモリーアドレスを更新します。7 行目では、2D ブロックストアを実行して、GRF 値をメモリーに書き戻します。
レーンごとのデフォルトのギャザー/スキャッター操作を備えた代替 SYCL* 実装と比較して、ESIMD でのブロック
ロード/ストアの明示的なプログラミングには、いくつかの利点があります。まず、ブロックメモリー操作は連続したメ
モリー位置にアクセスし、データの結合を容易にするため、より効率的です。次に、2D ブロックメッセージに許可される最大 GRF 戻りサイズは、スキャッター・メッセージよりも大きくなります。これにより、同じ量のデータをロードするのに必要なメッセージの総数が減り、メッセージキューとメモリーアービターの競合による潜在的なストールが回避されます。3 番目に、メッセージ・ペイロード・サイズが小さくなるため、GRF の負荷とロード/ストア・ユニット・バス・トラ
フィックが軽減されます。これは、ブロック・メッセージ・ペイロードにはブロック全体の開始 x 座標と y 座標の 2 つのスカラーしか含まれていないのに対し、スキャッター・メッセージ・ペイロードでは各レーンの開始座標をそれぞれ指定する必要があるためです。4 番目に、レーンごとのアドレスの計算に必要な命令が節約されます。これは、特に 64
ビットアドレスではコストがかかる可能性があります。
さらに、期待されるレイアウトでデータを GRF ブロックに直接ロードすることで、SIMD 計算スループットを向上できます。これは、前のセクションで説明した線形フィルターカーネルの例からわかります。2D ブロックメッセージは、マシンラーニング・ワークロードにとって重要な GEMM カーネル実装のピーク・パフォーマンスを実現するために、DotProduct-Accumulate-Systolic (DPAS) 操作の入力マトリックス・データをロードするためによく使用されます。
FP16 や INT8 な ど の 一 般 的 な デ ー タ タ イ プ の 場 合 、 DPAS は マ ト リ ッ ク ス B が Vector Neural Network
Instructions (VNNI) 形式で GRF に配置されていることを想定しています。これは、2D ブロックメッセージで変換
モードを指定することにより簡単に実現できます。データがレーンごとに分散したメッセージでロードされる場合、戻りデータはレーンごとに別々の GRF ブロックに書き込まれるため、データを所定の位置にシャッフルするために大幅に多くの命令が必要になり、パフォーマンスが低下します。
メモリーアクセスのレイテンシー隠匿するため、ESIMD は、メモリーシステム階層内でデータを近づけるためにプリフェッチを明示的に挿入する API もプログラマーに提供します。次のコードは、2D ブロックロードにプリフェッチを追加する方法を示しています。多くの場合、プリフェッチは、間に十分な他の有用な作業を実行したり、ループ間の反復を実行したりできるように、離れた場所に挿入されます。
1.
2.
3.
4.

120

1:
2:
3:
4:

constexpr uint32_t width = SurfaceWidth * sizeof(float) - 1;
constexpr uint32_t height = SurfaceHeight - 1;
constexpr uint32_t pitch = SurfacePitch * sizeof(float) - 1;
config_2d_mem_access<int, BlockWidth, BlockHeight, NumBlocks>

oneAPI GPU 最適化ガイド
5.
6.
7.
8.

4:
…
5:

payload(input, width, height, pitch, x, y);lsc_prefetch_2d(payload);
auto data = lsc_load_2d(payload);

もう 1 つの便利な機能は、キャッシュヒントを追加して、データをキャッシュするか、キャッシュしないか、ストリーミン
グするか、または他のキャッシュ制御ポリシーに従うかを示すことです。ESIMD は、基盤となるハードウェア・メッセー
ジ・オプションを完全に公開し、プログラマーがさまざまなキャッシュレベルで優先キャッシュ制御ポリシーを設定できるようにします。次のコードは、前の例の 2D ブロック・プリフェッチとロードにキャッシュヒントを追加する方法を示しています。最後の 2 つのテンプレート・パラメーターは、データを L1 キャッシュと L2 キャッシュの両方にキャッシュすることを指定するため追加されます。
1. 1:
2. 2:
3. 3:
4. 4:
5.
6. 4:
7.
8.
9. 5:
10.

constexpr uint32_t width = SurfaceWidth * sizeof(float) - 1;
constexpr uint32_t height = SurfaceHeight - 1;
constexpr uint32_t pitch = SurfacePitch * sizeof(float) - 1;
config_2d_mem_access<int, BlockWidth, BlockHeight, NumBlocks>
payload(input, width, height, pitch, x, y);lsc_prefetch_2d<int, BlockWidth, BlockHeight, NumBlocks, false,
false, cache_hint::cached, cache_hint::cached>(payload);…
auto data = lsc_load_2d<int, BlockWidth, BlockHeight, NumBlocks, false,
false, cache_hint::cached, cache_hint::cached> (payload);

Xe GPU メモリーシステムは、さまざまなアドレス空間へのアクセスを効率的にサポートします。アドレスモード、サイズ制限、その他のメッセージ・エンコーディングの不一致により、コンパイラーは異なるアドレス空間に対して異なるタイプのメッセージを生成する必要があります。通常、コンパイラーは、特定のメモリー操作に対してどのメッセージ
タイプを生成するか決定するため、アドレス空間を静的に推測しようとします。ただし、制御フローまたは関数呼び出
しで汎用ポインターが使用されている場合など、コンパイル時にアドレス空間を導出することが常に可能であるとは限りません。このような場合、コンパイラーはフォールバックして分岐コードを生成します。このコードは、基本的に実行時に動的チェックを実行してアドレス空間を決定し、正しいパスを実行します。これにより、コードサイズが増加し、パフォーマンスが大幅に低下する可能性があります。この問題を回避するため、ESIMD プログラマーは明示的なアドレス空間キャストを追加して、コンパイラーが適切なコード生成を選択できるようにすることができます。次のコード
は、ESIMD 関数にアドレス空間キャストを追加する方法を示しています。
1.
2.
3.
4.
5.
6.
7.
8.
9.

1: simd<float, 16> test(float *A, simd<float, 16> b, int i)
SYCL_ESIMD_FUNCTION {2:
simd<float, 16> a;
3:global_ptr<float, access::decorated::yes> ptr =sycl::address_space_cast<access::address_space::global_space,access::decorated::yes, float>(A);4:a.copy_from(ptr + i);5:
return a + b;
6: }1 行目では、関数パラメーター A が汎用ポインターとして宣言されています。実際のアドレス空間は呼び出しコンテ
キストによって異なり、コンパイル時に決定されない場合があります。3 行目では、アドレス空間キャストを追加して、A が指すデータがグローバルアドレス空間にあることを示します。これにより、コンパイラーはこの情報に基づいてグ
ローバル・メモリー・アクセス・メッセージを生成できます。
ESIMD で利用可能なメモリー API の完全なリストについては、以下を参照してください:
DPC++ ランタイム: メモリーアクセス API。(intel.github.io)

121

oneAPI GPU 最適化ガイド

5-4 GPU オフロードにライブラリーを使用
oneAPI ツールキットでは、最適化されたアプリケーションで使用できる特殊な API を提供することで、プログラミン
グを簡素化できるライブラリーを用意しています。この章では、アプリケーションを加速するコードサンプルを含むライブラリーの利用手順について説明します。利用可能な API を含む各ライブラリーの詳細は、それぞれのライブラリーのドキュメントに記載されています。
••••

パフォーマンス・ライブラリーを使用
SYCL* カーネルで共有ライブラリー関数を使用
oneAPI マス・カーネル・ライブラリー (oneMKL) を使用した効率良いフーリエ相関の実装
インテル® Xe マトリクス拡張による行列乗算のパフォーマンスの向上

5-4-1

パフォーマンス・ライブラリーを使用

ここでは、明示的にコードを記述する代わりに、oneAPI マス・カーネル・ライブラリー (oneMKL) や、oneAPI ディー
プ・ニューラル・ネットワーク・ライブラリー (oneDNN) などのライブラリーの効率良い関数を使用する方法を説明します。特定分野の数学演算を研究している専門家でない限り、通常、演算を行う独自バージョンを作成することは推奨しません。例えば、行列乗算は一般的で単純な数学演算です。

これは数行のコードで簡単に実装できます。
// A と B の行列乗算
for (m = 0; m < M; m++) {for (n = 0; n < N; n++) {C[m][n] = 0.0;
for (k = 0; k < K; k++) {C[m][n] += A[m][k] * B[k][n];}}} // 行列乗算の終わり

これは単純な実装ですが、最高のパフォーマンスを得られるとは限りません。内部ループをよく見ると、行列 B のメモ
リーアクセスが非連続であることが分かります。これは、キャッシュを再利用できずパフォーマンスが低下します。
このアルゴリズムを SYCL* に移植して、行列乗算カーネルをアクセラレーターで実行することは、それほど困難ではありません。次のコードは、キューを初期化してデフォルトデバイスにワークを送信し、統合共有メモリー (USM) に行
列空間を割り当てています。
// SYCL* キューを初期化
sycl::queue Q(sycl::default_selector_v);
auto sycl_device = Q.get_device();
auto sycl_context = Q.get_context();
std::cout << "Running on: "<< Q.get_device().get_info<sycl::info::device::name>() << std::endl;// USM に行列 A, B, および C を割り当て
auto A = sycl::malloc_shared<float *>(M, sycl_device, sycl_context);
for (m = 0; m < M; m++)A[m] = sycl::malloc_shared<float>(K, sycl_device, sycl_context);

122

oneAPI GPU 最適化ガイド

auto B = sycl::malloc_shared<float *>(K, sycl_device, sycl_context);
for (k = 0; k < K; k++)B[k] = sycl::malloc_shared<float>(N, sycl_device, sycl_context);
auto C = sycl::malloc_shared<float *>(M, sycl_device, sycl_context);
for (m = 0; m < M; m++)C[m] = sycl::malloc_shared<float>(N, sycl_device, sycl_context);// 行列 A, B, および C の初期化

USM のデータは、SYCL* ランタイムによってホストメモリーとデバイスメモリー間で移動できます。明示的なバッ
ファーは必要ありません。デフォルトのアクセラレーターにオフロードするため、計算は SYCL* カーネルに変換され、キューに送信されます。
// 行列乗算カーネルをオフロード
Q.parallel_for(sycl::range<2>{M, N}, [=](sycl::id<2> id) {unsigned int m = id[0];unsigned int n = id[1];float sum = 0.0;
for (unsigned int k = 0; k < K; k++)sum += A[m][k] * B[k][n];C[m][n] = sum;}).wait(); // 行列乗算の終わり

しかし、このようなコードをアクセラレーターにオフロードしても、パフォーマンスが向上する可能性はほとんどありません。実際には、パフォーマンスが低下します。適切に記述されていないコードは、ホストまたはデバイスで実行されているにかかわらず適切でないままです。
行列乗算など、計算量の多い一般的な演算は十分に研究されています。研究者は、基本的な数式を純粋に実装するよりも優れたパフォーマンスをもたらす多くのアルゴリズムを設計しました。それらは、キャッシュ・ブロッキングやルー
プアンロールなどのチューニングにより、行列 A および B の形状に関係なく高いパフォーマンスを実現します。
oneMKL は、最適化済みの一般的な行列乗算関数 (oneapi::mkl::blas::gemm) を提供し、ホスト・プロセッサー
または各種アクセラレーター・デバイスで高い性能を実現できます。行列は USM に割り当てられ、デバイスキュー、行
列次元、およびそのほか各種オプションが gemm 関数に渡されます。
// 行列乗算をオフロード
float alpha = 1.0, beta = 0.0;
oneapi::mkl::transpose transA = oneapi::mkl::transpose::nontrans;
oneapi::mkl::transpose transB = oneapi::mkl::transpose::nontrans;
sycl::event gemm_done;
std::vector<sycl::event> gemm_dependencies;
gemm_done = oneapi::mkl::blas::gemm(Q, transA, transB, M, N, K, alpha, A, M,
B, K, beta, C, M, gemm_dependencies);
gemm_done.wait();ライブラリー関数は、純粋な実装よりも高いパフォーマンスを示し、広範囲の用途に利用できます。例えば、ライブラリー関数は、必要に応じて乗算の前に一方または両方の行列を転置できます。この簡単な例は、アプリケーション開発者とチューニングの熟練者の関心がかけ離れていることを示しています。アプリケーション開発者は、高度に最適化されたライブラリーに一般的な計算をカプセル化するため熟練者に依存する傾向があります。oneAPI 仕様では、
アプリケーションの高速化に役立つ多くのライブラリーが定義されています。以下に例を示します。
••

数学演算向けの oneMKL
データ解析とマシンラーニング向けの oneDAL
123

oneAPI GPU 最適化ガイド••

マシンラーニング・フレームワークを開発するための oneDNN
ビデオ処理向けの インテル® VPL

必要な操作をスクラッチから実装する前に、oneAPI ライブラリーに利用可能な関数がないか確認してください。

5-4-2

SYCL* カーネルで共有ライブラリー関数を使用

すべてではありませんが、一部の標準 C++ 関数は SYCL* カーネル内から呼び出すことができます。関数の概要については、『データ並列 C++』 (英語) 書籍の第 18 章 (ライブラリー) を参照してください。サポートされない関数がSYCL* カーネルから呼び出された際に何が発生するか説明するため、簡単な例を示します。次のプログラムは、
rand() 関数を使用して乱数シーケンスを生成します。
1. #include <CL/sycl.hpp>
2. #include <iostream>
3. #include <random>4.
5. constexpr int N = 5;6.
7. extern SYCL_EXTERNAL int rand(void);
8.
9. int main(void) {10. #if defined CPU
11.
sycl::queue Q(sycl::cpu_selector_v);
12. #elif defined GPU
13.
sycl::queue Q(sycl::gpu_selector_v);
14. #else
15.
sycl::queue Q(sycl::default_selector_v);
16. #endif
17.
18.
std::cout << "Running on: "19.
<< Q.get_device().get_info<sycl::info::device::name>() << std::endl;
20.
21.
// SYCL* カーネル内で rand() を利用してみる
22.
auto test1 = sycl::malloc_shared<float>(N, Q.get_device(), Q.get_context());
23.
24.
srand((unsigned)time(NULL));
25.
Q.parallel_for(N, [=](auto idx) {26.
test1[idx] = (float)rand() / (float)RAND_MAX;27.
}).wait();28.
29.
// 乱数のシーケンスを表示
30.
for (int i = 0; i < N; i++)31.
std::cout << test1[i] << std::endl;
32.
33.
// クリーンアップ
34.
sycl::free(test1, Q.get_context());
35. }プログラムは、CPU (cpu_selector)、または GPU (gpu_selector) デバイスで実行される SYCL* カーネルにコンパイルできます。2 つのデバイスに対しエラーなしでコンパイルされ、CPU では正しく実行できますが、GPU で実行
すると失敗します。
$ icpx -fsycl -DCPU -std=c++17 external_rand.cpp -o external_rand$ ./external_rand
Running on: Intel(R) Xeon(R) E-2176G CPU @ 3.70GHz
0.141417
0.821271
0.898045
0.218854
0.304283

124

oneAPI GPU 最適化ガイド$ icpx -fsycl -DGPU -std=c++17 external_rand.cpp -o external_rand$ ./external_rand
Running on: intel® Graphics Gen9 [0x3e96]
terminate called after throwing an instance of 'cl::sycl::compile_program_error'
what(): The program was built for 1 devices
Build program log for 'Intel(R) Graphics Gen9 [0x3e96]':
error: undefined reference to `rand()'error: backend compiler failed build.
-11 (CL_BUILD_PROGRAM_FAILURE)
Abortedrand() への未定義参照があるため、ジャストインタイム (JIT) コンパイルで障害が発生し ます。この関数は
SYCL_EXTERNAL として宣言されていますが、GPU デバイスの rand() 関数に相当する SYCL* 関数がありません。
幸い、SYCL* ライブラリーには乱数生成関数など、多くの標準 C++ 関数の代替が含まれています。次の例は、oneAPI
DPC++ ライブラリー (oneDPL) (英語) と oneAPI マス・カーネル・ライブラリー (oneMKL) (英語) を使用した同等のコードを示しています。
1. #include <CL/sycl.hpp>
2. #include <iostream>
3. #include <oneapi/dpl/random>
4. #include <oneapi/mkl/rng.hpp>
5.
6. int main(int argc, char **argv) {7.
unsigned int N = (argc == 1) ? 20 : std::stoi(argv[1]);8.
if (N < 20)
9.
N = 20;
10.
11.
// oneDPL と oneMKL を使用して、[0.0, 1.0] 間の乱数シーケンスを生成
12.
//
13.
sycl::queue Q(sycl::gpu_selector_v);
14.
std::cout << "Running on: "15.
<< Q.get_device().get_info<sycl::info::device::name>() << std::endl;
16.
17.
auto test1 = sycl::malloc_shared<float>(N, Q.get_device(), Q.get_context());
18.
auto test2 = sycl::malloc_shared<float>(N, Q.get_device(), Q.get_context());
19.
20.
std::uint32_t seed = (unsigned)time(NULL); // RNG シード値を取得
21.
22.
// GPU デバイス上の oneDPL 乱数生成器
23.
24.
25.
26.
27.
28.

clock_t start_time = clock(); // タイマーを開始

29.
30.
31.
32.
33.
34.
35.
36.
37.

test1[idx] = rng_distribution(rng_engine); // RNG シーケンスを生成}).wait();

38.
39.
40.
41.

Q.parallel_for(N, [=](auto idx) {oneapi::dpl::minstd_rand rng_engine(seed, idx); // RNG エンジンを初期化
oneapi::dpl::uniform_real_distribution<float>
rng_distribution;// RNG 分布を設定

clock_t end_time = clock(); // タイマーを停止
std::cout << "oneDPL took " << float(end_time - start_time) / CLOCKS_PER_SEC<< " seconds to generate " << N<< " uniformly distributed random numbers." << std::endl;// GPU デバイス上の oneMKL 乱数生成器
start_time = clock(); // タイマーを開始
oneapi::mkl::rng::mcg31m1 engine(Q, seed); // RNG エンジンを初期化し、RNG 分布を設定

125

oneAPI GPU 最適化ガイド
42.
oneapi::mkl::rng::uniform<float, oneapi::mkl::rng::uniform_method::standard>
43.
rng_distribution(0.0, 1.0);
44.
oneapi::mkl::rng::generate(rng_distribution, engine, N, test2)
45.
.wait(); // RNG シーケンスを生成
46.
47.
end_time = clock(); // タイマーを停止
48.
std::cout << "oneMKL took " << float(end_time - start_time) / CLOCKS_PER_SEC
49.
<< " seconds to generate " << N
50.
<< " uniformly distributed random numbers."<< std::endl;
51.
52.
// 各メソッドの最初の 10 個の乱数を表示
53.
std::cout << std::endl
54.
<< "oneDPL"
55.
<< "\t"56.
<< "oneMKL" << std::endl;
57.
for (int i = 0; i < 10; i++)58.
std::cout << test1[i] << " " << test2[i] << std::endl;
59.
60.
// 各メソッドの最後の 10 個の乱数を表示
61.
std::cout << "..." << std::endl;
62.
for (size_t i = N - 10; i < N; i++)63.
std::cout << test1[i] << " " << test2[i] << std::endl;
64.
65.
// クリーンアップ
66.
sycl::free(test1, Q.get_context());
67.
sycl::free(test2, Q.get_context());
68. }oneDPL および oneMKL 関数は、それぞれ <oneapi/dpl/random> と <oneapi/mkl/rng.hpp> に含まれています。oneDPL と oneMKL の例では同じ操作のシーケンスを実行します。クロックから乱数シードを取得して、乱数エンジンを初期化し、目的とする乱数分布を選択して乱数を生成します。oneDPL コードは、明示的に SYCL* カーネルを使用してデバイスのオフロードを実行します。oneMKL コードでは、mkl::rng 関数がデバイスのオフロードを暗黙的に処理します。

5-4-3

oneAPI マス・カーネル・ライブラリー (oneMKL) を使用した効率良いフーリエ相関の実装

これまで oneMKL カーネル関数の簡単な使い方を説明しましたが、相互相関と呼ばれるより複雑な数学演算を見てみましょう。相互相関には多くの用途があります。例えば、2 つの 1D 信号の類似性を測定し、類似したイメージをオー
バーレイする最適な変換方法を検出する医療イメージの体積セグメント化などがあります。
1 と 0 のベクトルとして表現される信号について考えてみます。
Signal 1:
Signal 2:

0
0

0
0

0
1

0
1

0
0

1
0

1
0

0
0

シグナルは循環シフトとして扱われるため、2 番目のシグナルを最初のシグナルに対して 3 要素シフトすると、最大相関は 2 となります。
Signal 1: 0 0 0 0 0 1 1 0
Signal 2:
0 0 1 1 0 0 0 0
Correlation: (1 * 1) + (1 * 1) = 2

126

oneAPI GPU 最適化ガイド
2 または 4 要素のシフトは、1 の相関スコアを与えます。それ以外のシフトでは相関スコアはゼロになります。これは以下のように計算されます。

ここで、N はシグナルベクトルの要素数、i は sig1 に対する sig2 のシフトです。
実際のシグナルには多くのデータ (およびノイズ) が含まれますが、1D シグナルの位置に合わせ、2D イメージのオー
バーレイ、3D 立体イメージ・レジストレーションのいずれを実行する場合でも原理は同じです。ここでの目的は、相関を最大化する変換を見つけることです。ただし、上記の総当たりの合計では、N シフトごとに N 回の乗算と加算が必要になります。1D、2D、および 3D では、問題は O(N2)、O(N3) 、および O(N4) となります。
フーリエ相関アルゴリズムは、フーリエ変換 O(NlogN) の利点を活用するため、この計算を実行するはるかに効率良い方法と言えます。
corr = IDFT(DFT(sig1) * CONJG(DFT(sig2)))

ここで DFT は離散フーリエ変換、IDFT は逆 DFT、CONJG は複素共役です。フーリエ相関アルゴリズムは、最適化された順方向および逆方向の変換と、複素共役の乗算関数をサポートする oneMKL を使用して構成できます。つまり、計算全体をアクセラレーター・デバイスで実行できます。
多くのアプリケーションでは、最終的な結果のみが必要であるため、デバイスからホストに転送するデータはこれだけで済みます。
この例では、2 つの人工的なシグナルがデバイスで生成され、インプレースで変換された後に相関されます。ホストは最終結果を取得して、最善の変換と相関のストアをレポートします。これまでの経験では、バッファリングはホストとデバイス間のデータ転送を明示的に制御できるため、最高のパフォーマンスが得られると言われていました。
この仮説を検証するため、2 つの入力シグナルを生成してみます。
// シグナルデータのバッファーを作成。これはデバイスでのみ使用されます。
sycl::buffer<float> sig1_buf{N + 2};
sycl::buffer<float> sig2_buf{N + 2};// 相関結果を保持するコンテナを宣言// (デバイスで計算され、ホストで使用されます)
std::vector<float> corr(N + 2);

ニューラル・ネットワークの訓練中に過適合を防止するため、イメージに視覚効果を追加するため、または最適ではな
い検出器から取得したシグナルの検出能を向上させるため、シグナルにランダムノイズが追加されることがあります。
ここでは、oneMKL の単純な乱数生成器を使用してバッファーをランダムノイズで初期化します。
// 新しいスコープを開いて、相関結果の更新をトリガー{
sycl::buffer<float> corr_buf(corr);// 入力シグナルを初期化
std::uint32_t seed = (unsigned)time(NULL); // RNG シード値を取得
oneapi::mkl::rng::mcg31m1 engine(Q, seed); // RNG エンジンを初期化

127

oneAPI GPU 最適化ガイド// RNG 分布を設定
oneapi::mkl::rng::uniform<float, oneapi::mkl::rng::uniform_method::standard>
rng_distribution(-0.00005, 0.00005);
oneapi::mkl::rng::generate(rng_distribution, engine, N, sig1_buf); //ノイズ
oneapi::mkl::rng::generate(rng_distribution, engine, N, sig2_buf);

新しいスコープが開かれ、相関結果に対し corr_buf バッファーが宣言されていることに注目してください。このバッファーがスコープ外になると、ホストで corr が更新されます。
上記の例と同様に、人工的なシグナルはそれぞれのバッファーの両端に配置されます。
Q.submit([&](sycl::handler &h) {sycl::accessor sig1_acc{sig1_buf, h, sycl::write_only};
sycl::accessor sig2_acc{sig2_buf, h, sycl::write_only};h.single_task<>([=]() {sig1_acc[N - N / 4 - 1] = 1.0;
sig1_acc[N - N / 4] = 1.0;
sig1_acc[N - N / 4 + 1] = 1.0; // シグナル
sig2_acc[N / 4 - 1] = 1.0;
sig2_acc[N / 4] = 1.0;
sig2_acc[N / 4 + 1] = 1.0;});}); // シグナル初期化の終わり

シグナルが準備できたら、oneMKL の DFT 関数で変換します。
// FFT 記述子を初期化
oneapi::mkl::dft::descriptor<oneapi::mkl::dft::precision::SINGLE,
oneapi::mkl::dft::domain::REAL>
transform_plan(N);transform_plan.commit(Q);// 配列で順方向変換を実行
oneapi::mkl::dft::compute_forward(transform_plan, sig1_buf);
oneapi::mkl::dft::compute_forward(transform_plan, sig2_buf);

単精度の実数から複素数への順方向変換が SYCL* キューにコミットされ、両方のバッファーデータに対し、インプ
レース DFT が実行されます。DFT (sig1) の結果に CONJG(DFT(sig2)) を乗算します。これは手動で記述されたカーネルで実行できます。
Q.submit([&](sycl::handler &h){
sycl::accessor sig1_acc{sig1_buf, h, sycl::read_only};
sycl::accessor sig2_acc{sig2_buf, h, sycl::read_only};
sycl::accessor corr_acc{corr_buf, h, sycl::write_only};h.parallel_for<>(sycl::range<1>{N/2}, [=](auto idx){
corr_acc[idx*2+0] = sig1_acc[idx*2+0] * sig2_acc[idx*2+0] +sig1_acc[idx*2+1] * sig2_acc[idx*2+1];
corr_acc[idx*2+1] = sig1_acc[idx*2+1] * sig2_acc[idx*2+0] sig1_acc[idx*2+0] * sig2_acc[idx*2+1];
});
});

128

oneAPI GPU 最適化ガイド
ただし、この実装では、クロスアーキテクチャーの最適なパフォーマンスを得られる可能性はほとんどありません。幸い、このステップには oneMKL 関数 oneapi::mkl::vm::mulbyconj が利用できます。mulbyconj (英語) 関数
は、std::complex<float> の入力を想定しますが、バッファーは float データタイプで初期化されています。順
方向変換後に複素数データが含まれている場合、バッファーを再度キャストする必要があります。
auto sig1_buf_cplx =sig1_buf.template reinterpret<std::complex<float>, 1>((N + 2) / 2);
auto sig2_buf_cplx =sig2_buf.template reinterpret<std::complex<float>, 1>((N + 2) / 2);
auto corr_buf_cplx =corr_buf.template reinterpret<std::complex<float>, 1>((N + 2) / 2);
oneapi::mkl::vm::mulbyconj(Q, N / 2, sig1_buf_cplx, sig2_buf_cplx,
corr_buf_cplx);

IDFT ステップは計算を完了します。
// 複素数相関配列で逆交換を実行
oneapi::mkl::dft::compute_backward(transform_plan, corr_buf);

この例の始めで開かれたスコープが閉じられると、相関結果を保持するバッファーはスコープ外となり、ホストコンテ
ナが強制的に更新されます。これは、ホストとデバイス間の唯一のデータ転送です。
明示的なバッファリングを使用したフーリエ相関の実装を以下に示します。
1. #include <CL/sycl.hpp>
2. #include <iostream>
3. #include <mkl.h>
4. #include <oneapi/mkl/dfti.hpp>
5. #include <oneapi/mkl/rng.hpp>
6. #include <oneapi/mkl/vm.hpp>
7.
8. int main(int argc, char **argv) {9.
unsigned int N = (argc == 1) ? 32 : std::stoi(argv[1]);10.
if ((N % 2) != 0)
11.
N++;
12.
if (N < 32)
13.
N = 32;14.
15.
// SYCL* キューを初期化
16.
sycl::queue Q(sycl::default_selector_v);
17.
std::cout << "Running on: "18.
<< Q.get_device().get_info<sycl::info::device::name>() << std::endl;
19.
20.
// シグナルデータのバッファーを作成。これはデバイスでのみ使用されます。
21.
sycl::buffer<float> sig1_buf{N + 2};
22.
sycl::buffer<float> sig2_buf{N + 2};
23.
24.
// 相関結果を保持するコンテナを宣言
25.
26.
27.
28.
29.
30.
31.
32.

// (デバイスで計算され、ホストで使用されます)
std::vector<float> corr(N + 2);// 新しいスコープを開いて、相関結果の更新をトリガー{
sycl::buffer<float> corr_buf(corr);// 入力シグナルを初期化

33.

std::uint32_t seed = (unsigned)time(NULL); // RNG シード値を取得

34.

oneapi::mkl::rng::mcg31m1 engine(Q, seed); // RNG エンジンを初期化

35.

// RNG 分布を設定

129

oneAPI GPU 最適化ガイド
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.

130

oneapi::mkl::rng::uniform<float, oneapi::mkl::rng::uniform_method::standard>
rng_distribution(-0.00005, 0.00005);
oneapi::mkl::rng::generate(rng_distribution, engine, N, sig1_buf);//ノイズ
oneapi::mkl::rng::generate(rng_distribution, engine, N, sig2_buf);
Q.submit([&](sycl::handler &h) {sycl::accessor sig1_acc{sig1_buf, h, sycl::write_only};
sycl::accessor sig2_acc{sig2_buf, h, sycl::write_only};h.single_task<>([=]() {sig1_acc[N - N / 4 - 1] = 1.0;
sig1_acc[N - N / 4] = 1.0;
sig1_acc[N - N / 4 + 1] = 1.0; // シグナル
sig2_acc[N / 4 - 1] = 1.0;
sig2_acc[N / 4] = 1.0;
sig2_acc[N / 4 + 1] = 1.0;});}); // シグナル初期化の終わり
clock_t start_time = clock(); // タイマーを開始// FFT 記述子の初期化
oneapi::mkl::dft::descriptor<oneapi::mkl::dft::precision::SINGLE,
oneapi::mkl::dft::domain::REAL>
transform_plan(N);transform_plan.commit(Q);// 配列で順方向変換を実行
oneapi::mkl::dft::compute_forward(transform_plan, sig1_buf);
oneapi::mkl::dft::compute_forward(transform_plan, sig2_buf);// 計算: DFT(sig1) * CONJG(DFT(sig2))
auto sig1_buf_cplx =sig1_buf.template reinterpret<std::complex<float>, 1>((N + 2) / 2);
auto sig2_buf_cplx =sig2_buf.template reinterpret<std::complex<float>, 1>((N + 2) / 2);
auto corr_buf_cplx =corr_buf.template reinterpret<std::complex<float>, 1>((N + 2) / 2);
oneapi::mkl::vm::mulbyconj(Q, N / 2, sig1_buf_cplx, sig2_buf_cplx,
corr_buf_cplx);// 複素数相関配列で逆交換を実行
oneapi::mkl::dft::compute_backward(transform_plan, corr_buf);
clock_t end_time = clock(); // タイマーを停止
std::cout << "The 1D correlation (N = " << N << ") took "<< float(end_time - start_time) / CLOCKS_PER_SEC << " seconds."<< std::endl;} // 相関結果を保持するバッファーがスコープ外になり、// ホストコンテナの更新が強制されました// 最大相関値を与えるシフトを検出
float max_corr = 0.0;
int shift = 0;
for (unsigned int idx = 0; idx < N; idx++) {if (corr[idx] > max_corr) {max_corr = corr[idx];
shift = idx;}}
int _N = static_cast<int>(N);shift =

oneAPI GPU 最適化ガイド
99.

(shift > _N / 2) ? shift - _N : shift; // シグナルを循環的に処理

100.
// 互いのバージョンをシフト
101. std::cout << "Shift the second signal " << shift
102.
<< " elements relative to the first signal to get a maximum, "103.
"normalized correlation score of "104.
<< max_corr / N << "."<< std::endl;
105. }

明示的なバッファリングと比較するため、統合共有メモリー (USM) を使用してフーリエ相関アルゴリズムを再実装します。2 つの実装の違いがハイライト表示されています。最初に、シグナルと相関配列が USM に割り当てられ、次に人工的なデータで初期化されます。
// シグナルと相関配列を初期化
auto sig1 = sycl::malloc_shared<float>(N + 2, sycl_device, sycl_context);
auto sig2 = sycl::malloc_shared<float>(N + 2, sycl_device, sycl_context);
auto corr = sycl::malloc_shared<float>(N + 2, sycl_device, sycl_context);// 入力シグナルを初期化
std::uint32_t seed = (unsigned)time(NULL); // RNG シード値を取得
oneapi::mkl::rng::mcg31m1 engine(Q, seed); // RNG エンジンを初期化// RNG 分布を設定
oneapi::mkl::rng::uniform<float, oneapi::mkl::rng::uniform_method::standard>
rng_distribution(-0.00005, 0.00005);// 警告: この文はデバイスで実行されます。
auto evt1 =oneapi::mkl::rng::generate(rng_distribution, engine, N, sig1); // ノイズ
auto evt2 = oneapi::mkl::rng::generate(rng_distribution, engine, N, sig2);
evt1.wait();evt2.wait();// 警告: これらの文はホスト上で実行されるため、sig1 と sig2 を// デバイス上で更新する必要があります.
sig1[N - N / 4 - 1] = 1.0;
sig1[N - N / 4] = 1.0;
sig1[N - N / 4 + 1] = 1.0; // シグナル
sig2[N / 4 - 1] = 1.0;
sig2[N / 4] = 1.0;
sig2[N / 4 + 1] = 1.0;

USM ポインターが SYCL* バッファーではなく oneMKL 関数に渡されることを除いて、残りの実装はほとんど同一です。
// 配列で順方向変換を実行
evt1 = oneapi::mkl::dft::compute_forward(transform_plan, sig1);
evt2 = oneapi::mkl::dft::compute_forward(transform_plan, sig2);// 計算: DFT(sig1) * CONJG(DFT(sig2))
oneapi::mkl::vm::mulbyconj(
Q, N / 2, reinterpret_cast<std::complex<float> *>(sig1),
reinterpret_cast<std::complex<float> *>(sig2),
reinterpret_cast<std::complex<float> *>(corr), {evt1, evt2}).wait();// 複素数相関配列で逆交換を実行
oneapi::mkl::dft::compute_backward(transform_plan, corr).wait();

割り当てたメモリーを解放することも重要です。
131

oneAPI GPU 最適化ガイド
sycl::free(sig1, sycl_context);
sycl::free(sig2, sycl_context);
sycl::free(corr, sycl_context);

USM 実装には、見慣れた構文が多くあります。また、SYCL* ランタイムによって処理される暗黙のデータ転送に依存するため、概念も単純です。ただし、プログラマーによる誤りはパフォーマンスを低下させます。
次のコードの警告メッセージに注目してください。oneMKL 乱数生成エンジンはデバイスで初期化されるため、sig1と sig2 はデバイスでランダムノイズを使用して初期化されます。人工的なシグナルを追加するコードはホストで実行されることから、SYCL* ランタイムはホストとデバイス間でデータの一貫性を維持する必要があります。フーリエ相関で使用されるシグナルは、3D イメージのアプリケーションでは特に巨大化するため、不要なデータ転送はパフォー
マンスの低下につながります。
デバイス上でシグナルデータを直接更新すると、データの一貫性が維持されるため不要なデータ転送を回避できます。
Q.single_task<>([=]() {sig1[N - N / 4 - 1] = 1.0;
sig1[N - N / 4] = 1.0;
sig1[N - N / 4 + 1] = 1.0; // シグナル
sig2[N / 4 - 1] = 1.0;
sig2[N / 4] = 1.0;
sig2[N / 4 + 1] = 1.0;}).wait();

明示的なバッファリングと USM の実装は同等のパフォーマンスを達成でき、(プログラマーがデータの一貫性を考慮していれば) SYCL* ランタイムが不要なデータ転送を回避できることを示しています。
USM を使用したフーリエ相関の実装を以下に示します。
1. #include <CL/sycl.hpp>
2. #include <iostream>
3. #include <mkl.h>
4. #include <oneapi/mkl/dfti.hpp>
5. #include <oneapi/mkl/rng.hpp>
6. #include <oneapi/mkl/vm.hpp>
7.
8. int main(int argc, char **argv) {9.
unsigned int N = (argc == 1) ? 32 : std::stoi(argv[1]);10.
if ((N % 2) != 0)
11.
N++;
12.
if (N < 32)
13.
N = 32;14.
15.
// SYCL* キューを初期化
16.
sycl::queue Q(sycl::default_selector_v);
17.
auto sycl_device = Q.get_device();18.
auto sycl_context = Q.get_context();
19.
std::cout << "Running on: "20.
<< Q.get_device().get_info<sycl::info::device::name>() << std::endl;
21.
22.
// シグナルと相関配列を初期化
23.
auto sig1 = sycl::malloc_shared<float>(N + 2, sycl_device, sycl_context);
24.
auto sig2 = sycl::malloc_shared<float>(N + 2, sycl_device, sycl_context);
25.
auto corr = sycl::malloc_shared<float>(N + 2, sycl_device, sycl_context);
26.
27.
// 入力シグナルを初期化
28.

std::uint32_t seed = (unsigned)time(NULL); // RNG シード値を取得

29.

oneapi::mkl::rng::mcg31m1 engine(Q, seed); // RNG エンジンを初期化

30.

132

// RNG 分布を設定

oneAPI GPU 最適化ガイド
31.

oneapi::mkl::rng::uniform<float,
oneapi::mkl::rng::uniform_method::standard>
32.
rng_distribution(-0.00005, 0.00005);
33.
34.
auto evt1 =35.
oneapi::mkl::rng::generate(rng_distribution, engine, N, sig1); // ノイズ
36.
auto evt2 = oneapi::mkl::rng::generate(rng_distribution, engine, N, sig2);37.
evt1.wait();38.
evt2.wait();
39.
40.
Q.single_task<>([=]() {41.
sig1[N - N / 4 - 1] = 1.0;42.
sig1[N - N / 4] = 1.0;
43.
sig1[N - N / 4 + 1] = 1.0; // シグナル44.
sig2[N / 4 - 1] = 1.0;45.
sig2[N / 4] = 1.0;
46.
sig2[N / 4 + 1] = 1.0;
47.
}).wait();48.
49.
clock_t start_time = clock(); // タイマーを開始
50.
51.
// FFT 記述子の初期化
52.
oneapi::mkl::dft::descriptor<oneapi::mkl::dft::precision::SINGLE,
53.
oneapi::mkl::dft::domain::REAL>
54.
transform_plan(N);55.
transform_plan.commit(Q);56.
57.
// 配列で順方向変換を実行
58.
evt1 = oneapi::mkl::dft::compute_forward(transform_plan, sig1);
59.
evt2 = oneapi::mkl::dft::compute_forward(transform_plan, sig2);
60.
61.
// 計算: DFT(sig1) * CONJG(DFT(sig2))
62.
oneapi::mkl::vm::mulbyconj(
63.
Q, N / 2, reinterpret_cast<std::complex<float> *>(sig1),
64.
reinterpret_cast<std::complex<float> *>(sig2),
65.
reinterpret_cast<std::complex<float> *>(corr), {evt1, evt2})
66.
.wait();
67.
68.
// 複素数相関配列で逆変換を実行
69.
oneapi::mkl::dft::compute_backward(transform_plan, corr).wait();70.
71.
clock_t end_time = clock(); // タイマーを停止
72.
std::cout << "The 1D correlation (N = " << N << ") took "73.
<< float(end_time - start_time) / CLOCKS_PER_SEC << " seconds."
74.
<< std::endl;
75.
76.
// 最大相関値を与えるシフトを検出
77.
float max_corr = 0.0;
78.
int shift = 0;79.
for (unsigned int idx = 0; idx < N; idx++) {80.
if (corr[idx] > max_corr) {81.
max_corr = corr[idx];
82.
shift = idx;
83.
}
84.
}
85.
int _N = static_cast<int>(N);86.
shift =
87.
(shift > _N / 2) ? shift - _N : shift; // シグナルを循環的に処理
88.
89.
90.
91.
92.
93.

// 互いのバージョンをシフト
std::cout << "Shift the second signal " << shift<< " elements relative to the first signal to get a maximum, ""normalized correlation score of "<< max_corr / N << "."<< std::endl;

133

oneAPI GPU 最適化ガイド
94.
// クリーンアップ
95.
sycl::free(sig1, sycl_context);
96.
sycl::free(sig2, sycl_context);
97.
sycl::free(corr, sycl_context);
98. }

最大相関値の場所を特定する最後のステップは、ホストで実行されることに注意してください。入力データが大量にある場合、この計算をデバイスで実行することを推奨します。幸いなことに、maxloc リダクションは SYCL* で実装できる汎用的な並列パターンです。これは皆さんへの演習として残しておきますが、『データ並列 C++』 (英語) の図 1411 に役立つ例が示されています。

5-4-4インテル® Xe マトリクス拡張による行列乗算のパフォーマンスの向上

今日、世界では人工知能 (AI) の人気が高まっており、アプリケーションのパフォーマンスを向上させるため、低精度のデータタイプとこれらのデータタイプをサポートするハードウェアの導入が求められています。低精度モデルは計算が高速で、メモリー・フットプリントが小さくなります。同じ理由で、float32 がデフォルトのデータタイプであるにもかかわらず、低精度のデータタイプは AI / マシンラーニング(ML) のトレーニングと推論の両方で広く使用されるようになっています。これらの低精度のデータタイプを最適化してサポートするには、特別なハードウェア機能と命令が必要です。インテルは、GPU でインテル® Xe マトリクス拡張 (インテル® XMX) でそれらを提供しています。最もよく使用される 16 ビット形式と 8 ビット形式には、float16 (fp16)、bfloat16 (bf16)、16 ビット整数 (int16)、8 ビット整数(int8) などがあります。次の図は、これらの形式の違いを視覚化したものです。

上図では、s は符号付きビット (2 進数表現の最初の桁、0 は正の数、1 は負の数を意味します) であり、exp は指数です。

5-4-4-1

インテル® Xe マトリクス拡張

インテル® Xe マトリクス拡張 (インテル® XMX) は、2D シストリック・アレイ上での Dot Product Accumulate
Systolic (DPAS) 命令の実行に特化しています。並列コンピューター・アーキテクチャーのシストリック・アレイは、密に結合されたデータ処理ユニットの同種ネットワークです。各ユニットは、上流の隣接ユニットから受信したデータの関数として部分的な結果を計算し、その結果を内部に格納して下流に渡します。インテル® XMX は、ハードウェア世代に応じて、int8、fp16、bf16、tf32 など、さまざまなデータタイプをサポートしています。インテル® データセンター
GPU マックス・シリーズの インテル® XMX を理解するには、インテル® Iris® Xe GPU アーキテクチャーのセクションを参照してください。

134

oneAPI GPU 最適化ガイド

5-4-4-2

インテル® XMX のプログラミング

ユーザーは、ディープラーニング・フレームワーク、専用ライブラリー、カスタム SYCL* カーネルから低レベルの組み込み関数まで、さまざまなレベルで XMX を操作できます。インテル® XMX を使用してアプリケーションをプログラミ
ングして実行するには、インテル® oneAPI ベース・ツールキットが必要です。
インテル® oneAPI ディープ・ニューラル・ネットワーク・ライブラリー (oneDNN) を使用
ハードウェアを最大限に活用するため、oneDNN はデフォルトでインテル® GPU (インテル® Xe 第 4 世代スケーラブ
ル・プロセッサー以降) で インテル® XMX のサポートを有効にしています。XMX および oneDNN でサポートされているデータタイプを使用するには、GPU サポートを有効にしてアプリケーションを構築する必要があります。
インテル® oneAPI マス・カーネル・ライブラリー (oneMKL) を使用
oneDNN と同様に、oneMKL でも、サポートされているデータタイプを使用し、コードが インテル® oneAPI DPC++
コンパイラーでコンパイルされている場合、デフォルトでインテル® XMX が有効になります。
oneMKL は、XMX を使用して単精度 gemm と gemm_batch を高速化するいくつかのアルゴリズムをサポートしています。bf16x2 と bf16x3 は、bf16 を使用して単精度 gemm を近似する 2 つのアルゴリズムです。
内部的に単精度の入力データは bf16 に変換され、シストリック・アレイで乗算されます。3 つのバリアント (bf16、
bf16x2、bf16x3) により、精度とパフォーマンスのトレードオフが可能になります。bf16 が最も高速で、bf16x3 が最も正確です (標準の単精度 gemm の精度に似ています)。例の行列乗算では、これらのアルゴリズムの使用方法を示し、下の表ではパフォーマンスの違いを比較しています。
精度

データタイプ/アルゴリズム

ピーク (TF)

単精度
単精度
単精度
単精度

fp32
bf16
bf16x2
bf16x3

26
151
74
42

単精度の理論上のピークに対するパフォーマンス(%)
98
577
280
161

135

oneAPI GPU 最適化ガイド
テストは、Ubuntu* 22.04 を実行する 512 GB DDR5-4800 + インテル® データセンターGPU MAX 1550 を搭載したインテル® Xeon® 8480+ で実行されました。
この表は、bf16、bf16x2、bf16x3 のパフォーマンスが単精度の理論上のピークをはるかに上回っていることを示しています。精度のトレードオフが許容できる場合は、bf16、bf16x2、bf16x3 の順で使用することを強く推奨します。

5-4-4-3

関連資料

1. マトリクス乗算のパフォーマンス (英語)
2. マトリクス乗算 (英語)

5-5 ホスト/デバイスメモリー、バッファー、および USM
アクセラレーターは、いくつかのメモリー階層にアクセスできます。最高のパフォーマンスを得るには、階層内の適切なレベルを使用することが重要です。
ここでは、メモリー階層の宣言、移動、およびアクセスに関連するトピックについて説明します。
API を使用すると、異なるデバイスプロセス間でメモリー・オブジェクトを共有できます。各プロセスには独自の仮想アドレス空間があるため、メモリー・オブジェクトが新しいプロセスで共有されるときに同じ仮想アドレスが使用可能になるという保証はありません。メモリー・オブジェクトの共有を容易にする一連の API があります。
メモリー共有に oneAPI レベル 0 API を使用する方法の詳細については、レベル 0 仕様のプロセス間通信を参照してください。
統合共有メモリーの割り当て
USM とバッファーのパフォーマンスへの影響
ホストとデバイス間でデータ移動の往復を避ける
データ転送の最適化ループ内でのバッファー宣言を避ける
バッファー・アクセサー・モード








5-6 統合共有メモリーの割り当て
統合共有メモリー (USM) を利用すると、プログラムは C/C++ ポインターを使用してメモリーにアクセスできます。
SYCL* でメモリーを割り当てるには、3 つの方法があります。
malloc_device:•••

割り当てられたメモリーは、指定されたデバイスからのみアクセスできますが、コンテキスト内のほかのデバ
イスやホストからはアクセスできません。
このデータは常にデバイス上にあるため、カーネル実行の最速の選択肢です。
コンテキスト内のホストやほかのデバイスにデータを転送するには、明示的なコピー操作が必要です。

malloc_host:••136

割り当てられたメモリーは、ホストとコンテキスト内のほかのデバイスからアクセスできます。
データは常にホスト上にあり、デバイスからは PCIe* 経由でアクセスされます。

oneAPI GPU 最適化ガイド•

ホストとデバイス間のデータを同期するのに明示的なコピー操作は必要ありません。

malloc_shared:•••

割り当てられたメモリーのアクセスは、ホストと指定されたデバイスのみが許可されます。
ホストとデバイス間でデータを移行して (ゼロレベルドライバーによって行われる)、アクセスを高速化できます。
ホストとデバイス間の同期には明示的なコピー操作は必要ありませんが、コンテキスト内のデバイスには必要です。

3 つのメモリー割り当てとその特徴を以下にまとめます。
メモリー割り当てタイプと特徴
メモリー割り当てタイプ
hostdevice

説明
ホストメモリーの割り当て

デバイスメモリーの割り当て
ホストとデバイス間で共有割り当てsharedホストからアクセス可能
はい

デバイスからアクセス
可能
はい

いいえ

PCIe* またはファブ
リック・リンクを介してリモートアクセス可能
はい

はい

はい

場所
ホスト

デバイス
ホストとデバイス間で動的に移行

マルチスタック、マルチ C スライス GPU 環境では、デバイスと共有される USM 割り当てがルートデバイスに関連付
けられていることに注意してください。したがって、同じデバイス上のすべてのスタックと C スライスからアクセスできます。混乱を避けるため、プログラムは malloc_device と malloc_shared 割り当てにルートデバイスを使用する必要があります。

5-6-1

OpenMP* USM 割り当て APISYCL* USM モデルに合わせて、開発者がアプリケーション、メモリーサイズ、およびパフォーマンス要件に基づいて
メモリー割り当てを行えるよう、インテル拡張機能として 3 つの新しい OpenMP* API が追加されています。それらのセマンティクスとパフォーマンス特性については、次の節で詳しく説明します。
ホストメモリー割り当て
ホスト割り当ては、ホストとデバイスの間でデータを手動でコピーする必要がないため、デバイス割り当てよりも容易に利用できます。ホスト割り当ては、ホストとデバイスの両方からアクセス可能なホストメモリーへの割り当てです。
この割り当ては、デバイスからアクセスできますが、デバイスに接続されたメモリーに移行することはできません。
代わりに、このメモリーに対する読み書きを行うオフロード領域は、PCIe* バスまたはファブリック・リンクを介してリモートで実行します。これには利便性とパフォーマンスのトレードオフがあります。ホスト割り当てによって生じるアク
セスコストは高くなりますが、ホスト割り当てを利用する利点はほかにもあります。例えば、ほとんどアクセスされないデータや、デバイスに接続されたメモリーに収まらない大きなデータセットが含まれます。ホストメモリー割り当てを要求する API を次に示します。
extern void *omp_target_alloc_host(size_t size, int device_num)137

oneAPI GPU 最適化ガイド
デバイスメモリー割り当て
この割り当ては、GDDR/DDR やデバイス上の HBM など、デバイスに接続されたメモリーへのポインターアクセスを利用する際に必要となります。デバイスに割り当てられたメモリーは、デバイス上で実行される領域をオフロードすることで読み書きできますが、ホスト上で実行されるコードからは直接アクセスすることはできません。ホスト上のデバ
イス割り当てにアクセスすると、誤ったデータを取得したり、プログラムがクラッシュすることがあります。デバイスメ
モリー割り当てを要求する API を次に示します。
extern void *omp_target_alloc_device(size_t size, int device_num)

共有メモリー割り当て
ホスト割り当てと同様に、共有割り当ては、ホストとデバイスの両方からアクセスできます。ホスト割り当てとの違いは、
共有割り当てでは、ホストメモリーとデバイスに接続されたメモリー間で、プログラマーからは透過に自動でデータを移行できることです。割り当てがデバイスに移行されると、デバイスにアクセスするオフロード領域は、ホストからリ
モートでアクセスするよりも高いパフォーマンスで実行されます。ただし、共有割り当てには、ページの移行コストやピンポン効果などのコストが生じるため、すべてのケースで利点が得られるわけではありません。
extern void *omp_target_alloc_shared(size_t size, int device_num)

omp_target_alloc API の USM サポート
ターゲットメモリー割り当て用の OpenMP* API は、以下のようになります。
extern void *omp_target_alloc_device(size_t size, int device_num)

5-7 USM とバッファーのパフォーマンスへの影響
SYCL* は、デバイスのメモリーを管理するいくつかの選択肢を提供します。ここでは、パフォーマンスのトレードオフについて説明します。始めに概念を簡単に紹介します。詳しい説明は、『データ並列 C++』 (英語) を参照してください。
他の言語と同様に動作は仕様によって定義されますが、実装は定義されないため、パフォーマンス特性はソフトウェ
アのバージョンとデバイスで異なる可能性があります。これについて考えてみます。
バッファー: バッファーはデバイスとホストからアクセスできるデータのコンテナです。SYCL* ランタイムは、メモリーの割り当て、読み取り、書き込み向けの API を提供することでメモリーを管理します。ランタイムは、ホストとデバイス間でデータを移動し、データアクセスを同期する役割を持ちます。
統合共有メモリー (USM): データへのアクセスが API でのみ行われるバッファーとは異なり、USM では従来のC/C++ ポインターを使用してデータを読み書きできます。USM には 2 つのバリアントがあります。デバイスに割り当てられたメモリーにはデバイスからのみアクセスできるため、ホストとデバイス間で明示的にデータを移動する必要があります。共有割り当ては、ランタイムが自動的にメモリーを移動することで、デバイスまたはホストから参照できます。
3 つのモデルのモデルで作成された同じサンプルプログラムを比較して、選択肢のトレードオフを確認します。問題を解決するため、GPU とホストが連携して計算するプログラムを使用するにはデータを送受信する必要があります。
最初にシリアル計算から見ていきましょう。7 行目のループを GPU で実行し、11 行目のループを CPU で実行すると考えます。両方のループでデータ配列の読み書きを行うため、6 行目のループ反復ごとにホストと GPU 間でデータを移動する必要があります。
138

oneAPI GPU 最適化ガイド
1. void serial(int stride) {2. // data の割り当てと初期化
3. float *data = new float[data_size];
4. init(data);
5.
6. timer it;
7.
8. for (int i = 0; i < time_steps; i++) {9.
for (int j = 0; j < data_size; j++) {10.
for (int k = 0; k < device_steps; k++)11.
data[j] += 1.0;
12.
}
13.
14.
for (int j = 0; j < data_size; j += stride)15.
data[j] += 1.0;
16. }17. put_elapsed_time(it);18.
19. check(data);
20.
21. delete[] data;
22. } // シリアル

5-7-1

バッファー

次に、バッファーを使用してデータを管理する計算を示します。3 行目でバッファーを作成して init 関数で初期化します。init 関数はここでは示されませんが、アクセサーまたはポインターを受け入れます。parallel_for は 11行目で定義されるカーネルを実行します。カーネルは、device_data アクセサーを使用して buffer_data のデータを読み書きしますが、ここでは、コードがデータの位置を指定していないことに注目してください。
アクセサーはデータが必要なタイミングと位置を明示し、SYCL* ランタイムがデータをデバイスに移動して (必要であれば)、カーネルを起動します。21 行目の host_accessor は、データがホスト上で読み取り/書き込みされることを示します。カーネルは buffer_data も読み取り/書き込みするため、host_accessor コンストラクターはカーネルが完了するのを待機して、データをホストに移動した後、23 行目で読み取り/書き込みを実行します。ループの次の反復では、11 行目のアクセサー・コンストラクターがデバイスにデータが戻されるまで待機します。これにより、カーネルの起動が遅延します。
1. void buffer_data(int stride) {2. // ホストでバッファーを割り当てて初期化
3. sycl::buffer<float> buffer_data{data_size};
4. init(sycl::host_accessor(buffer_data, sycl::write_only, sycl::no_init));5.
6. timer it;
7. for (int i = 0; i < time_steps; i++) {8.
9.
// デバイスで計算
10.
q.submit([&](auto &h) {11.
sycl::accessor device_data(buffer_data, h);12.
13.
auto compute = [=](auto id) {14.
for (int k = 0; k < device_steps; k++)15.
device_data[id] += 1.0;
16.
};
17.
h.parallel_for(data_size, compute);
18.
});
19.
20.
// ホストで計算
21.
sycl::host_accessor host_data(buffer_data);22.
for (int i = 0; i < data_size; i += stride)23.
host_data[i] += 1.0; }139

oneAPI GPU 最適化ガイド
24.
}
25.
put_elapsed_time(it);26.
27.
const sycl::host_accessor h(buffer_data);
28.
check(h);29. } // buffer_data

パフォーマンス上の考察
15 行目と 23 行目のデータアクセスは単純な配列参照のように見えますが、C++ オペレーターのオーバーロードとSYCL* ランタイムによって実装されています。アクセサーによる配列参照の効率は実装によって異なりますが、デバイ
スコードはダイレクトメモリー参照と比較してもオーバーロードに対するコストは生じません。ランタイムは、バッ
ファーのどのデータがアクセスされるか事前に認識できないため、カーネルが開始される前にすべてのデータがデバ
イス上に存在することを確認する必要があります。これは、今後変わる可能性があります。
同様のことが host_accessor にも当てはまります。ランタイムはすべてのデータをホストに移動するわけではありません。配列参照は複雑なコードで実装されており、ネイティブ C++ による配列参照に比べるとかなり低速です。少量のデータ参照は許容されますが、host_accessor を使用する計算量の多いアルゴリズムは、パフォーマンスの観点から回避する必要があります。
もう 1 つの問題は並列性です。host_accessor は、アクセサーがデータの読み取りと書き込みにあまり使用されていなくても、同じバッファーを参照するカーネルの起動をブロックできます。host_accessor を含むスコープを可能な限り最小限にします。この例では、4 行目のホストアクセサーは init 関数からリターンした後に破棄され、21 行目のホストアクセサーはそれぞれのループ反復が終了すると破棄されます。

5-7-2

共有割り当て次に、共有割り当てにより実装される同様のアルゴリズムを示します。データは 2 行目で割り当てられます。USM に割り当てられるデータは、従来のポインターでアクセスできるためアクセサーを必要としません。したがって、10 行目と 15 行目の配列参照は、単純なインデックス操作で実装できます。12 行目の parallel_for は、ホストが 15 行
目でデータにアクセスする前にカーネルが完了するのを待機して終了します。バッファーと同様に、SYCL* ランタイム
はカーネルを起動する前にすべてのデータがデバイス上に存在することを確認します。また、共有割り当ては参照されない限りホストにコピーされません。ホストで最初にデータを参照すると、オペレーティング・システムのページフォル
トが発生して、データページがデバイスからホストにコピーされ実行が続けられます。後続のコードが同じページのデータを参照する場合、遅延なくアクセスできます。カーネルが起動されると、ホスト上の常駐ページはすべてデバイ
スにフラッシュバックされます。
1. void shared_usm_data(int stride) {2.
float *data = sycl::malloc_shared<float>(data_size, q);3.
init(data);
4.
5.
timer it;6.
7.
for (int i = 0; i < time_steps; i++) {8.
auto compute = [=](auto id) {9.
for (int k = 0; k < device_steps; k++)10.
data[id] += 1.0;
11.
};
12.
q.parallel_for(data_size, compute).wait();13.
14.
for (int k = 0; k < data_size; k += stride)15.
data[k] += 1.0;
16.
}
17.
q.wait();18.
put_elapsed_time(it);
19.

140

oneAPI GPU 最適化ガイド
20.
check(data);
21.
22.
sycl::free(data, q);23. } // shared_usm_data

パフォーマンス上の考察
バッファーと比較すると、データ参照は単純なポインターであり、良好なパフォーマンスを提供します。ただし、データをホストに転送するためページフォルトが処理されると、データ転送のコストに加えオーバーヘッドが発生します。
アプリケーションへの影響は参照パターンによって異なります。スパース・ランダム・アクセスはオーバーヘッドが最も高く、データの線形スキャンはページフォルトの影響が低いです。
すべての同期はプログラマーによる明示的な制御下にあるため、うまく設計されたプログラムでは同時実行性は問題になりません。

5-7-3

デバイス割り当て

デバイス割り当てを使用するプログラムを以下に示します。デバイスに割り当てられたデータは、デバイス上でのみ直接アクセス可能であり、21 行目にあるようにホストに明示的にコピーする必要があります。デバイスとホスト間の同期はすべて明示的です。21 行目は待機してから終了するため、非同期コピーが完了するまでホストコードの実行は開始されません。キュー定義は示されていませんが、インオーダーでキューを使用するため 21 行目の memcpy は 18 行
目の parallel_for が完了するのを待機します。
1. void device_usm_data(int stride) {2.
// ホストデータの割り当てと初期化
3.
float *host_data = new float[data_size];4.
init(host_data);
5.
6.
// デバイスデータを割り当て
7.
float *device_data = sycl::malloc_device<float>(data_size, q);
8.
9.
timer it;10.
11.
for (int i = 0; i < time_steps; i++) {12.
// デバイスへデータをコピーして計算
13.
q.memcpy(device_data, host_data, sizeof(float) * data_size);
14.
auto compute = [=](auto id) {15.
for (int k = 0; k < device_steps; k++)16.
device_data[id] += 1.0;
17.
};
18.
q.parallel_for(data_size, compute);
19.
20.
// ホストへデータをコピーして計算
21.
q.memcpy(host_data, device_data, sizeof(float) * data_size).wait();22.
for (int k = 0; k < data_size; k += stride)23.
host_data[k] += 1.0;
24.
}
25.
q.wait();26.
put_elapsed_time(it);27.
28.
check(host_data);
29.
30.
sycl::free(device_data, q);31.
delete[] host_data;
32. } // device_usm_data141

oneAPI GPU 最適化ガイド
パフォーマンス上の考察
データの移動と同期はどちらも明示的であり、プログラマーの完全な制御下にあります。配列参照は、ホスト上で行われるため、共有割り当てのページフォルトおよびバッファーに関連するオーバーロードのオーバーヘッドもありません。共有割り当ては、ホストが実際に参照するデータだけをメモリーページ単位で転送します。理論的には、デバイ
ス割り当ては任意の粒度によるオンデマンドの移動を可能にします。データを細粒度で非同期移動することは可能
ですが、複雑になる可能性があるため大部分のプログラマーはデータ構造全体を一度だけ転送して済ませます。明
示的なデータ移動と同期によりコードは複雑になりますが、デバイス割り当てにより高いパフォーマンスを得ることができます。

5-8 ホストとデバイス間でデータ移動の往復を避ける
特にディスクリート・アクセラレーター・デバイスでは、ホストとデバイス間でデータを移動するコストは高くなります。
そのため、ホストとデバイス間のデータ転送は可能な限り回避することが重要です。状況によっては、アクセラレー
ター上のカーネルによって計算されたデータをホストに移動して、何らかの操作を行い、処理を続行するためデバイスに送り返すことが必要な場合があります。そのような場合、デバイスからホストへの転送、およびホストからデバイスへの転送コストが生じることになります。
次の例を考えてみます。ここでは、カーネルが何らかの操作 (この場合ベクトル加算) により新しいベクトルに結果を生成します。次に、このベクトルは、それぞれの値に関数を適用することで別のベクトルに変換され、追加の計算を行
うため別のカーネルの入力として供給されます。このような計算は非常に一般的であり、多くの分野で使用されます。
ここでは、アルゴリズムは反復的で、1 つの計算の出力を別の計算の入力として供給する必要があります。典型的な例
として、計算レイヤーとして構造化されるマシンラーニング・モデルがありますが、この場合 1 つのレイヤーの出力が次のレイヤーの入力となります。
1. double myFunc1(sycl::queue &q, AlignedVector<int> &a, AlignedVector<int> &b,
2.
AlignedVector<int> &c, AlignedVector<int> &d,3.
AlignedVector<int> &res, int iter) {4.
sycl::range num_items{a.size()};5.
VectorAllocator<int> alloc;
6.
AlignedVector<int> sum(a.size(), alloc);
7.
8.
const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};9.
sycl::buffer a_buf(a, props);10.
sycl::buffer b_buf(b, props);11.
sycl::buffer c_buf(b, props);
12.
sycl::buffer d_buf(b, props);
13.
sycl::buffer res_buf(res, props);
14.
sycl::buffer sum_buf(sum.data(), num_items, props);
15.
16.
Timer timer;17.
for (int i = 0; i < iter; i++) {18.
// カーネル 1
19.
q.submit([&](auto &h) {20.
// 入力アクセサー
21.
sycl::accessor a_acc(a_buf, h, sycl::read_only);22.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
23.
// 出力アクセサー
24.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
25.
26.
h.parallel_for(num_items,
27.
[=](auto id) { sum_acc[id] = a_acc[id] + b_acc[id]; });
28.
});
29.
30.
{
31.
sycl::host_accessor h_acc(sum_buf);32.
for (int j = 0; j < a.size(); j++)142

oneAPI GPU 最適化ガイド33.
if (h_acc[j] > 10)34.
h_acc[j] = 1;
35.
else
36.
h_acc[j] = 0;
37.
}
38.
39.
// カーネル 2
40.
q.submit([&](auto &h) {41.
// 入力アクセサー
42.
sycl::accessor sum_acc(sum_buf, h, sycl::read_only);43.
sycl::accessor c_acc(c_buf, h, sycl::read_only);44.
sycl::accessor d_acc(d_buf, h, sycl::read_only);45.
// 出力アクセサー
46.
sycl::accessor res_acc(res_buf, h, sycl::write_only, sycl::no_init);
47.
48.
h.parallel_for(num_items, [=](auto id) {49.
res_acc[id] = sum_acc[id] * c_acc[id] + d_acc[id];
50.
});
51.
});
52.
q.wait();53.
}
54.
double elapsed = timer.Elapsed() / iter;55.
return (elapsed);56. } // myFunc1 の終わり

データをホストに移動して関数でそのデータを処理し、デバイス上の次のカーネルにデータを送る代わりに、カーネル
3 を作成してデバイス自身でこの関数を実行できます。この手法を次の例に示します。これは、機能的には前のコードと同じです。ここでは、カーネル 1 とカーネル 2 の間の accum_buf の中間データを操作する、第 3 のカーネルである
カーネル 3 について説明します。これには、デバイスからホストへのデータの送り戻しを回避できる利点があります。
1. double myFunc2(sycl::queue &q, AlignedVector<int> &a, AlignedVector<int> &b,
2.
AlignedVector<int> &c, AlignedVector<int> &d,3.
AlignedVector<int> &res, int iter) {4.
sycl::range num_items{a.size()};5.
VectorAllocator<int> alloc;
6.
AlignedVector<int> sum(a.size(), alloc);
7.
8.
const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};9.
sycl::buffer a_buf(a, props);10.
sycl::buffer b_buf(b, props);11.
sycl::buffer c_buf(b, props);
12.
sycl::buffer d_buf(b, props);
13.
sycl::buffer res_buf(res, props);
14.
sycl::buffer sum_buf(sum.data(), num_items, props);
15.
16.
Timer timer;17.
for (int i = 0; i < iter; i++) {18.
// カーネル 1
19.
q.submit([&](auto &h) {20.
// 入力アクセサー
21.
sycl::accessor a_acc(a_buf, h, sycl::read_only);22.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
23.
// 出力アクセサー
24.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
25.
26.
h.parallel_for(num_items,
27.
[=](auto i) { sum_acc[i] = a_acc[i] + b_acc[i]; });
28.
});
29.
30.
// カーネル 3
31.
q.submit([&](auto &h) {32.
sycl::accessor sum_acc(sum_buf, h, sycl::read_write);33.
h.parallel_for(num_items, [=](auto id) {143

oneAPI GPU 最適化ガイド
34.
if (sum_acc[id] > 10)35.
sum_acc[id] = 1;
36.
else
37.
sum_acc[id] = 0;
38.
});
39.
});
40.
41.
// カーネル 2
42.
q.submit([&](auto &h) {43.
// 入力アクセサー
44.
sycl::accessor sum_acc(sum_buf, h, sycl::read_only);45.
sycl::accessor c_acc(c_buf, h, sycl::read_only);46.
sycl::accessor d_acc(d_buf, h, sycl::read_only);47.
// 出力アクセサー
48.
sycl::accessor res_acc(res_buf, h, sycl::write_only, sycl::no_init);
49.
50.
h.parallel_for(num_items, [=](auto i) {51.
res_acc[i] = sum_acc[i] * c_acc[i] + d_acc[i];
52.
});
53.
});
54.
q.wait();55.
}
56.
double elapsed = timer.Elapsed() / iter;57.
return (elapsed);58. } // myFunc2 の終わり

この例を最適化する方法はほかにもあります。例えば、カーネル 3 のクリッピング操作を次のようにカーネル 1 の計
算にマージします。これはカーネルの融合と呼ばれ、3 番目のカーネルを起動しない利点もあります。SYCL* コンパイ
ラーは、このような最適化を行いません。マシンラーニングなど特定のドメインには、ML モデルを操作して同じ影響を及ぼす操作を融合するグラフ・コンパイラーがあります。
1. double myFunc3(sycl::queue &q, AlignedVector<int> &a, AlignedVector<int> &b,
2.
AlignedVector<int> &c, AlignedVector<int> &d,3.
AlignedVector<int> &res, int iter) {4.
sycl::range num_items{a.size()};5.
VectorAllocator<int> alloc;
6.
AlignedVector<int> sum(a.size(), alloc);
7.
8.
const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};9.
sycl::buffer a_buf(a, props);10.
sycl::buffer b_buf(b, props);11.
sycl::buffer c_buf(b, props);
12.
sycl::buffer d_buf(b, props);
13.
sycl::buffer res_buf(res, props);
14.
sycl::buffer sum_buf(sum.data(), num_items, props);
15.
16.
Timer timer;17.
for (int i = 0; i < iter; i++) {18.
// カーネル 1
19.
q.submit([&](auto &h) {20.
// 入力アクセサー
21.
sycl::accessor a_acc(a_buf, h, sycl::read_only);22.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
23.
// 出力アクセサー
24.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
25.
26.
h.parallel_for(num_items, [=](auto i) {27.
int t = a_acc[i] + b_acc[i];28.
if (t > 10)29.
sum_acc[i] = 1;
30.
else
31.
sum_acc[i] = 0;
32.
});

144

oneAPI GPU 最適化ガイド
33.
});
34.
35.
// カーネル 2
36.
q.submit([&](auto &h) {37.
// 入力アクセサー
38.
sycl::accessor sum_acc(sum_buf, h, sycl::read_only);39.
sycl::accessor c_acc(c_buf, h, sycl::read_only);40.
sycl::accessor d_acc(d_buf, h, sycl::read_only);
41.
// 出力アクセサー
42.
sycl::accessor res_acc(res_buf, h, sycl::write_only, sycl::no_init);
43.
44.
h.parallel_for(num_items, [=](auto i) {45.
res_acc[i] = sum_acc[i] * c_acc[i] + d_acc[i];
46.
});
47.
});
48.
q.wait();49.
}
50.
double elapsed = timer.Elapsed() / iter;51.
return (elapsed);52. } // myFunc3 の終わり

次のコードに示すように、このカーネルの融合をさらに進めてカーネル 1 とカーネル 2 の両方を融合します。これにより、カーネルの起動が排除されることに加えて、中間 accum_buf のメモリーを排除することで高いパフォーマンスが得られます。この場合、パフォーマンス上の利点の大部分は、メモリー参照の局所性の改善によるものです。
1. double myFunc4(sycl::queue &q, AlignedVector<int> &a, AlignedVector<int> &b,
2.
AlignedVector<int> &c, AlignedVector<int> &d,3.
AlignedVector<int> &res, int iter) {4.
sycl::range num_items{a.size()};5.
VectorAllocator<int> alloc;
6.
7.
const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};8.
sycl::buffer a_buf(a, props);9.
sycl::buffer b_buf(b, props);10.
sycl::buffer c_buf(b, props);11.
sycl::buffer d_buf(b, props);
12.
sycl::buffer res_buf(res, props);
13.
14.
Timer timer;15.
for (int i = 0; i < iter; i++) {16.
// カーネル 1
17.
q.submit([&](auto &h) {18.
// 入力アクセサー
19.
sycl::accessor a_acc(a_buf, h, sycl::read_only);20.
sycl::accessor b_acc(b_buf, h, sycl::read_only);21.
sycl::accessor c_acc(c_buf, h, sycl::read_only);22.
sycl::accessor d_acc(d_buf, h, sycl::read_only);
23.
// 出力アクセサー
24.
sycl::accessor res_acc(res_buf, h, sycl::write_only, sycl::no_init);
25.
26.
h.parallel_for(num_items, [=](auto i) {27.
int t = a_acc[i] + b_acc[i];28.
if (t > 10)29.
res_acc[i] = c_acc[i] + d_acc[i];
30.
else
31.
res_acc[i] = d_acc[i];
32.
});
33.
});
34.
q.wait();35.
}
36.
double elapsed = timer.Elapsed() / iter;37.
return (elapsed);38. } // myFunc4 の終わり

145

oneAPI GPU 最適化ガイド

5-9 データ転送の最適化
5-9-1

はじめに

前のセクションでは、ホストとデバイス間のデータ転送を最小限に抑える方法について説明しました。このセクション
では、ホストとデバイス間でデータを移動する必要がある場合にデータ転送を高速化する方法について説明します。
ホストとデバイス間でデータを繰り返し移動する場合、ソースとデスティネーションの両方が統合共有メモリー(USM) 内にあると、データ転送速度が最大化されます。
このセクションでは、システムメモリー (malloc または new で割り当てられたメモリーなど) の代わりにホスト統合
共有メモリー (USM) を使用するために SYCL* および OpenMP* API を呼び出すことでパフォーマンスを向上させる
方法を示します。

5-9-2

SYCL* でデータ転送を最適化

SYCL* では、ホストとデバイス間のデータ転送は、SYCL* memcpy 関数を使用して明示的に行うことも、SYCL* バッ
ファーとアクセサーを使用して暗黙的に行うこともできます。
ケース 1: バッファーを使用したデータ転送
事前に割り当てられたシステムメモリーへのポインターを使用して構築された SYCL バッファーの場合、SYCL* ラン
タイムはバッファーの作成時にホスト USM を使用し、バッファーの破棄時にホスト USM を解放する機能を備えています。この機能を有効にするには、次のように、ホストポインターを使用してバッファーを作成する必要があります。
1.
2.

int* hostptr = (int*)malloc(4*sizeof(int));
buffer<int, 1> Buffer(hostptr,4);

実行時に環境変数 SYCL_USM_HOSTPTR_IMPORT=1 を設定する必要があります。
ケース 2: SYCL* データ移動 API を使用したデータ転送
ホストデータの割り当てがユーザーの制御下にあり、割り当てられたポインターがデータ転送 API で使用される場合、ユーザーは malloc_host などの USM 関数を使用して、システムメモリーではなくホスト USM メモリーを割り当てることができます。
メモリーが割り当てられているソースコードが利用できない、または変更できない場合は、効率的なデータ転送のために、最初のデータ転送の前にシステムメモリーをインポート (デバイスコピー用に準備) し、すべての使用とデータ転送が完了した後に解放することができます。
インポート (準備) とリリース用の API セットが提供されています。これにより、プログラマーはアドレス範囲とインポートの期間を明示的に制御できます。
SYCL* の実験的な準備とリリース APISYCL* の準備およびリリース API のインターフェイスは次のとおりです。
1.
2.
3.
4.

146

void* sycl::prepare_for_device_copy(void* ptr, size_t numBytes, sycl::context& syclContext)
void* sycl::prepare_for_device_copyoneAPI GPU 最適化ガイド
5. (void* ptr, size_t numBytes, sycl::queue& syclQueue)
6.
7. void sycl::release_from_device_copy
8. (void* ptr, sycl::context& syclContext)
9.
10. void sycl::release_from_device_copy
11. (void* ptr, sycl::queue& syclQueue)

API の説明については、sycl_ext_oneapi_copy_optimize を参照してください。
API は簡単に使用できますが、メモリー範囲の有効期間に関して正確性と安全性を確保する責任はユーザーにあり
ます。
注




numBytes 引数が malloc されたメモリーブロックのサイズと同じでない場合 (たとえば、malloc されたメモリーが 1024 バイトであるが、numBytes が 512 または 2048 である場合)、ここでのガイドは、データ転送サイズと一致するインポート (準備) のサイズを使用することです。実際の割り当てが大きい場合、実際の割り当てよりも少ない量をインポートしても悪影響はありません。実際の割り当てよりも多くをインポートすると、ユーザーエラーになります。
準備/リリース API は実験的なものであり、変更される可能性があります。

SYCL* の例
次の例 sycl_prepare_bench.cpp は、ホストとデバイス間のデータ転送速度を測定します。データ転送サイズは変更できます。プログラムは、デバイスからホスト、およびホストからデバイスへのデータ転送速度を、ギガバイト/秒
単位で出力します。プログラムに渡されるスイッチは、データ転送方向、SYCL* prepare API を使用するかどうか、および転送サイズの範囲を制御するために使用されます。スイッチは、例のソースにリストされています。
プログラムでは、次の呼び出しを使用して、最初にインポート (準備) が 1 回実行され、最後にリリースが 1 回実行さ
れます。
1.
2.
3.
4.

sycl::ext::oneapi::experimental::prepare_for_device_copy(
hostp, transfer_upper_limit, dq);
sycl::ext::oneapi::experimental::release_from_device_copy(hostp, dq);

上記の呼び出しの間には、ポインター hostp を含む memcpy を繰り返し実行するループがあります。
1. #include <math.h>
2. #include <stdlib.h>
3. #include <chrono>
4. #include <time.h>
5. #include <unistd.h>
6. #include <sycl/sycl.hpp>
7.
8. using namespace sycl;
9.
10. static const char *usage_str =11. "\n ze_bandwidth [OPTIONS]"
12. "\n"13. "\n OPTIONS:"
14. "\n -t, string
selectively run a particular test:"
15. "\nh2d or H2D
run only Host-to-Device tests"
16. "\nd2h or D2H
run only Device-to-Host tests "17. "\n[default: both]"18. "\n -q
minimal output"
19. "\n[default: disabled]"147

oneAPI GPU 最適化ガイド
20. "\n -v
enable verificaton"
21. "\n[default: disabled]"
22. "\n -i
set number of iterations per transfer"23. "\n[default: 500]"
24. "\n -s
select only one transfer size (bytes) "25. "\n -sb
select beginning transfer size (bytes)"
26. "\n[default: 1]"
27. "\n -se
select ending transfer size (bytes)"
28. "\n[default: 2^28]"
29. "\n -l
use SYCL prepare_for_device_copy/release_from_device_copy
APIs"30. "\n[default: disabled]"
31. "\n -h, --help
display help message"
32. "\n";33.
34. static uint32_t sanitize_ulong(char *in) {35. unsigned long temp = strtoul(in, NULL, 0);36. if (ERANGE == errno) {37. fprintf(stderr, "%s out of range of type ulong\n", in);38. } else if (temp > UINT32_MAX) {39. fprintf(stderr, "%ld greater than UINT32_MAX\n", temp);
40. } else {41. return static_cast<uint32_t>(temp);
42. }43. return 0;
44. }
45.
46. size_t transfer_lower_limit = 1;
47. size_t transfer_upper_limit = (1 << 28);
48. bool verify = false;
49. bool run_host2dev = true;
50. bool run_dev2host = true;
51. bool verbose = true;
52. bool prepare = false;
53. uint32_t ntimes = 500;54.
55. // kernel latency
56. int main(int argc, char **argv) {57. for (int i = 1; i < argc; i++) {58. if ((strcmp(argv[i], "-h") == 0) || (strcmp(argv[i], "--help") == 0)) {59. std::cout << usage_str;
60. exit(0);61. } else if (strcmp(argv[i], "-q") == 0) {62. verbose = false;
63. } else if (strcmp(argv[i], "-v") == 0) {64. verify = true;65. } else if (strcmp(argv[i], "-l") == 0) {66. prepare = true;67. } else if (strcmp(argv[i], "-i") == 0) {68. if ((i + 1) < argc) {69. ntimes = sanitize_ulong(argv[i + 1]);70. i++;
71. }72. } else if (strcmp(argv[i], "-s") == 0) {73. if ((i + 1) < argc) {74. transfer_lower_limit = sanitize_ulong(argv[i + 1]);75. transfer_upper_limit = transfer_lower_limit;
76. i++;
77. }78. } else if (strcmp(argv[i], "-sb") == 0) {79. if ((i + 1) < argc) {80. transfer_lower_limit = sanitize_ulong(argv[i + 1]);81. i++;
82. }83. } else if (strcmp(argv[i], "-se") == 0) {84. if ((i + 1) < argc) {85. transfer_upper_limit = sanitize_ulong(argv[i + 1]);86. i++;
87. }

148

oneAPI GPU 最適化ガイド
88. } else if ((strcmp(argv[i], "-t") == 0)) {89. run_host2dev = false;
90. run_dev2host = false;
91.
92. if ((i + 1) >= argc) {93. std::cout << usage_str;
94. exit(-1);
95. }96. if ((strcmp(argv[i + 1], "h2d") == 0) ||97. (strcmp(argv[i + 1], "H2D") == 0)) {98. run_host2dev = true;
99. i++;
100. } else if ((strcmp(argv[i + 1], "d2h") == 0) ||101. (strcmp(argv[i + 1], "D2H") == 0)) {102. run_dev2host = true;103. i++;104. } else {105. std::cout << usage_str;
106. exit(-1);
107. }
108. } else {109. std::cout << usage_str;
110. exit(-1);
111. }
112. }
113.
114. queue dq;
115. device dev = dq.get_device();116. size_t max_compute_units = dev.get_info<info::device::max_compute_units>();117. auto BE = dq.get_device()118. .template get_info<sycl::info::device::opencl_c_version>()119. .empty()
120. ? "L0"121. : "OpenCL";
122. if (verbose)
123. std::cout << "Device name " << dev.get_info<info::device::name>() << " "124. << "max_compute units"125. << " " << max_compute_units << ", Backend " << BE << "\n";126.
127. void *hostp;
128. posix_memalign(&hostp, 4096, transfer_upper_limit);
129. memset(hostp, 1, transfer_upper_limit);
130.
131. if (prepare) {132. if (verbose)
133. std::cout << "Doing L0 Import\n";134. sycl::ext::oneapi::experimental::prepare_for_device_copy(
135. hostp, transfer_upper_limit, dq);
136. }
137.
138. void *destp =139. malloc_device<char>(transfer_upper_limit, dq.get_device(), dq.get_context());
140. dq.submit([&](handler &cgh) { cgh.memset(destp, 2, transfer_upper_limit); });141. dq.wait();142.
143. if (run_host2dev) {144. if (!verbose)
145. printf("SYCL USM API (%s)\n", BE);
146. for (size_t s = transfer_lower_limit; s <= transfer_upper_limit; s <<= 1) {147. auto start_time = std::chrono::steady_clock::now();
148. for (int i = 0; i < ntimes; ++i) {149. dq.submit([&](handler &cgh) { cgh.memcpy(destp, hostp, s); });150. dq.wait();151. }
152. auto end_time = std::chrono::steady_clock::now();
153. std::chrono::duration<double> seconds = end_time - start_time;154.
155. if (verbose)
156. printf("HosttoDevice: %8lu bytes, %7.3f ms, %8.3g GB/s\n", s,149

oneAPI GPU 最適化ガイド
157. 1000 * seconds.count() / ntimes,
158. 1e-9 * s / (seconds.count() / ntimes));159. else
160. printf("%10.6f\n", 1e-9 * s / (seconds.count() / ntimes));161. }
162. }
163.
164. if (run_dev2host) {165. if (!verbose)
166. printf("SYCL USM API (%s)\n", BE);
167. for (size_t s = transfer_lower_limit; s <= transfer_upper_limit; s <<= 1) {168. auto start_time = std::chrono::steady_clock::now();
169. for (int i = 0; i < ntimes; ++i) {170. dq.submit([&](handler &cgh) { cgh.memcpy(hostp, destp, s); });171. dq.wait();172. }
173. auto end_time = std::chrono::steady_clock::now();174. std::chrono::duration<double> seconds = end_time - start_time;
175.
176. if (verbose)
177. printf("DeviceToHost: %8lu bytes, %7.3f ms, %8.3g GB/s\n", s,178. seconds.count(), 1e-9 * s / (seconds.count() / ntimes));179. else
180. printf("%10.6f\n", 1e-9 * s / (seconds.count() / ntimes));181. }
182. }
183.
184. if (prepare)
185. sycl::ext::oneapi::experimental::release_from_device_copy(hostp, dq);186.
187. free(hostp);
188. free(destp, dq.get_context());
189. }

コンパイルコマンド:$ icpx -fsycl sycl_prepare_bench.cpp

実行コマンド：$ a.out -p -t h2d -s 256000000

上記の実行コマンドは、準備 (-p)、ホストからデバイスへの転送 (-t h2d)、および 2 億 5,600 万バイトの転送 (-s256000000) を実行することを指定します。
データ転送速度測定
sycl_prepare_bench.cpp プログラムを使用して、ホストとデバイス間のデータ転送速度 (準備/解放ありとなし)
を測定します。

150

oneAPI GPU 最適化ガイド
使用した特定の GPU (1 スタックのみ) で実行したデータサイズ 2 億 5,600 万バイトの転送速度は次のようになりました。
実行コマンド

転送方向

準備/リリース

転 送 レ ー ト

API を使用するか?

(GB/秒)

a.out -t h2d -s 256000000

ホストからデバイス

いいえ

26.9

a.out -p -t h2d -s 256000000

ホストからデバイス

はい

45.4

a.out -t d2h -s 256000000

デバイスからホスト

いいえ

33.2

a.out -p -t d2h -s 256000000

デバイスからホスト

はい

48.0

5-9-3

OpenMP* でデータ転送最適化

OpenMP* では、インポート (準備) と解放は、OpenMP* のインテル拡張機能として提供されるレジスター API とアンレジスター API を使用して行われます。これにより、プログラマーはアドレス範囲とインポートの期間を明示的に制御できます。
OpenMP* レジスターおよびアンレジスターAPIs
OpenMP* レジスターおよびアンレジスター API のインターフェイスは次のとおりです。
1.
2.
3.
4.
5.

int ompx_target_register_host_pointer(void *HostPtr, size_t numBytes, int DeviceNum)
void ompx_target_unregister_host_pointer(void *HostPtr, int DeviceNum)

API の説明については、「インテル® コンパイラーの OpenMP* 拡張ルーチン (C/C++)」 (英語) を参照してください。
OpenMP* のレジスターおよびアンレジスター API は、SYCL* の準備およびリリース API に似ています。API を使用する場合、有効期間の範囲に関して正確性と安全性を確保する責任はユーザーにあります。
注

レジスター/アンレジスターAPI は、C/C++ コードだけでなく Fortran コードからも呼び出すことができます。

OpenMP* の例
以下に、メモリー割り当てがパフォーマンスに与える影響を示す 3 つの OpenMP* の例 (Fortran 形式) を示します。
例では、主な計算は iter 反復のループで実行され、iter は 2000 です。
target data ディレクティブは、ループの開始前に行列 a、b、c をデバイスにマップし、ループの終了時に行列 c を
デバイスからホストにコピーするのに使用されます。
ループの各反復では、ホスト上で a と b の要素が更新され、target update ディレクティブを使用して新しい値がデバイスにコピーされます。その後、デバイス上で計算される sgemm が呼び出されます。sgemm はデバイス上で行列a と b を乗算し、その結果をデバイス上の行列 c に格納します。

151

oneAPI GPU 最適化ガイド
主な計算は、次のコードに示されています。
!$omp target data map(to: a, b) map(tofrom: c)do i = 1, iter
! Update arrays a and b on the host.
a(:,:) = a(:,:) + 1b(:,:) = b(:,:) - 1! Copy new values of a and b to the device.
!$omp target update to (a, b)! Compute sgemm on the device.
!$omp dispatch
call sgemm('n','n',m,n,k,alpha,a,m,b,k,beta,c,m)
end do!$omp end target data

3 つの OpenMP* の例のそれぞれにおけるメモリー割り当てについて、以下で説明します。
例 1: システムメモリーに行列を割り当て
最初の OpenMP* の例 openmp_system_mem.f では、行列 a、b、c が Fortran の allocate ステートメントを使用してシステムメモリーに割り当てられます。
allocate( a(1 : m, 1 : k) )allocate( b(1 : k, 1 : n) )allocate( c(1 : m, 1 : n) )例 2: ホストの USM に行列を割り当て
2 番目の OpenMP* の例 openmp_host_usm.f では、アロケーター omp_target_host_mem_alloc を指定した OpenMP allocators ディレクティブを使用して、行列 a、b、c がホスト USM に割り当てられます。
!$omp allocators allocate(allocator(omp_target_host_mem_alloc): a)
allocate( a(1 : m, 1 : k) )!$omp allocators allocate(allocator(omp_target_host_mem_alloc): b)
allocate( b(1 : k, 1 : n) )!$omp allocators allocate(allocator(omp_target_host_mem_alloc): c)
allocate( c(1 : m, 1 : n) )例 3: システムメモリーに行列を割り当て、レジスター (登録)/アンレジスター (登録解除) API を使用します。
3 番目の OpenMP* の例 openmp_register_mem.f では、openmp_system_mem.f と同様に、Fortran の
allocate ステートメントを使用して、行列 a、b、c がシステムメモリーに割り当てられます。
allocate( a(1 : m, 1 : k) )allocate( b(1 : k, 1 : n) )allocate( c(1 : m, 1 : n) )行列が割り当てられた直後に、行列のメモリーが登録（インポート）されます。
stat = ompx_target_register_host_pointer(C_LOC(a),sizeof(a), device_num)
stat = ompx_target_register_host_pointer(C_LOC(b),sizeof(b), device_num)
stat = ompx_target_register_host_pointer(C_LOC(c),sizeof(c), device_num)

152

&
&
&

oneAPI GPU 最適化ガイド
行列は割り当て解除される前に登録解除（解放）されます。
call ompx_target_unregister_host_pointer(C_LOC(a), device_num)
call ompx_target_unregister_host_pointer(C_LOC(b), device_num)
call ompx_target_unregister_host_pointer(C_LOC(c), device_num)

パフォーマンスの比較
3 つの OpenMP* サンプル、openmp_sys_usm.f、openmp_host.f、openmp_register_mem.f のパフォーマ
ンスを比較します。
コンパイルおよび実行コマンドは次のとおりです。
コンパイルコマンド:$ ifx -O3 -fiopenmp -fopenmp-targets=spir64 -qmkl -fpp -free openmp_system_mem.f -o
openmp_system_mem.exe$ ifx -O3 -fiopenmp -fopenmp-targets=spir64 -qmkl -fpp -free openmp_host_usm.f -o
openmp_host_usm.e$ ifx -O3 -fiopenmp -fopenmp-targets=spir64 -qmkl -fpp -free openmp_register_mem.f -o
openmp_register_mem.exe

実行コマンド:OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0 ./openmp_system_mem.exe
OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0 ./openmp_host_usm.exe
OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0 ./openmp_register_mem.exe

使用された特定の GPU (1 スタックのみ) で実行した場合の 3 つのバージョンのパフォーマンスは次のとおりです。
サンプル

合計時間 (秒)openmp_system_mem.f

5.885

openmp_host_usm.f

5.223

openmp_register_mem.f

5.225

上記の表では、ホスト USM (openmp_host_usm.f) に行列を割り当てると、システムメモリー(openmp_system_mem.f) に行列を割り当てるよりもパフォーマンスが優れていることが分かります。
システム・メモリー・バージョンのパフォーマンスは、API ompx_target_register_host_pointer() および
ompx_target_unregister_host_pointer() を呼び出して、ループ内の計算の前に行列を登録 (インポート)
し、ループ後に行列を登録解除 (解放) することで向上できます (openmp_register_mem.f)。その結果、
openmp_register_mem.f のパフォーマンスは openmp_host_usm.f のパフォーマンスと同等になっています。

153

oneAPI GPU 最適化ガイド

5-9-4

パフォーマンスに関する推奨事項

ホストとデバイス間で繰り返しデータ転送を行う場合は、次のアプローチを推奨します。
ホストとデバイス間の繰り返しデータ転送のソースまたはデスティネーションとなるデータは、システムメモ
リーではなく、ホストの統合共有メモリー (USM) に割り当てます。データをホスト USM に割り当てると、デー
タ転送速度が最適化されます。データをホスト USM に割り当てるには、次の手順を実行します。

1.





SYCL* では、malloc_host API を使用します。
OpenMP* C/C++ および Fortran では、omp_target_alloc_host API を使用します。
ま た は 、 OpenMP* Fortran の み で 、 ア ロ ケ ー タ ー omp_target_host_mem_alloc と と も に
allocators ディレクティブを使用します。

上記のアプローチ（1）を適用できない場合は、次の API を使用してシステムメモリーをインポートします。

2.




5-9-5
1.

SYCL* では、上記の prepare_for_device_copy および release_from_device_copy API を使用します。
OpenMP* (C/C++ および Fortran) では、上記の ompx_target_register_host_pointer および
ompx_target_unregister_host_pointer API を使用します。

関連資料

sycl_ext_oneapi_copy_optimize (英語)2.

OpenMP* へのインテル® コンパイラー拡張ルーチン (C/C++) (英語)3.

環境変数— oneAPI DPC++ コンパイラー・ドキュメント (英語)

5-10 ループ内でのバッファー宣言を避ける
カーネルが for ループ内で繰り返し起動される場合、ループ外部でバッファーを宣言することでバッファーの割り当てと解放の繰り返しを回避できます。ループ内でバッファーを宣言すると、ホストからデバイスへのメモリーコピーとデバイスからホストへのメモリーコピーが繰り返されます。
次の例では、for ループ内でカーネルが繰り返し起動されています。バッファー CBuf は一時配列として反復内で値を保持するために使用されていますが、1 回の反復で割り当てられた値は他の反復では使用されません。バッファー
CBuf はループ内で宣言されているため、ループ反復ごとに割り当てと解放が行われます。バッファーの割り当てと解放に加えて、バッファーに関連付けられたメモリーが反復ごとにホストからデバイスへ、そしてデバイスからホストへ
冗長的に転送されます。
1. #include <CL/sycl.hpp>
2. #include <stdio.h>3.
4. constexpr int N = 25;
5. constexpr int STEPS = 100000;
6.
7. int main() {8.
9.
int AData[N];10.
int BData[N];11.
int CData[N];12.
13.
sycl::queue Q;

154

oneAPI GPU 最適化ガイド
14.
15.
16.
17.
18.
19.
20.
21.

// それぞれ N 個の整数を保持する 2 つのバッファーを作成
sycl::buffer<int> ABuf(&AData[0], N);
sycl::buffer<int> BBuf(&BData[0], N);
Q.submit([&](auto &h) {// デバイスアクセサーを作成。
// プロパティー no_init は、バッファーの以前の22.
// 内容を破棄できることをランタイムに通知します。
23.
sycl::accessor aA(ABuf, h, sycl::write_only, sycl::no_init);
24.
sycl::accessor aB(BBuf, h, sycl::write_only, sycl::no_init);
25.
h.parallel_for(N, [=](auto i) {26.
aA[i] = 10;27.
aB[i] = 20;
28.
});
29.
});
30.
31.
for (int j = 0; j < STEPS; j++) {32.
sycl::buffer<int> CBuf(&CData[0], N);
33.
Q.submit([&](auto &h) {34.
// デバイスアクセサーを作成
35.
sycl::accessor aA(ABuf, h);36.
sycl::accessor aB(BBuf, h);37.
sycl::accessor aC(CBuf, h);38.
h.parallel_for(N, [=](auto i) {39.
aC[i] = (aA[i] < aB[i]) ? -1 : 1;
40.
aA[i] += aC[i];41.
aB[i] -= aC[i];
42.
});
43.
});
44.
} // for の終わり
45.
46.
// ホストアクセサーを作成
47.
const sycl::host_accessor haA(ABuf);48.
const sycl::host_accessor haB(BBuf);49.
printf("%d %d\n", haA[N / 2], haB[N / 2]);50.
51.
return 0;52. }より適切なアプローチは、for ループの前で CBuf を宣言することです。これにより、CBuf は一度だけ割り当てられループが終了するときに解放されます。さらに、ホストとデバイス間の冗長なデータ転送を排除できるためパフォーマンスが向上します。次のカーネルではこの変更を適用しています。
1. #include <CL/sycl.hpp>
2. #include <stdio.h>3.
4. constexpr int N = 25;
5. constexpr int STEPS = 100000;
6.
7. int main() {8.
9.
int AData[N];10.
int BData[N];11.
int CData[N];12.
13.
sycl::queue Q;
14.
15.
// それぞれ N 個の整数を保持する 3 つのバッファーを作成
16.
sycl::buffer<int> ABuf(&AData[0], N);17.
sycl::buffer<int> BBuf(&BData[0], N);18.
sycl::buffer<int> CBuf(&CData[0], N);
19.

155

oneAPI GPU 最適化ガイド
20.
21.
22.

Q.submit([&](auto &h) {// デバイスアクセサーを作成。
// プロパティー no_init は、バッファーの以前の23.
// 内容を破棄できることをランタイムに通知します。
24.
sycl::accessor aA(ABuf, h, sycl::write_only, sycl::no_init);
25.
sycl::accessor aB(BBuf, h, sycl::write_only, sycl::no_init);
26.
h.parallel_for(N, [=](auto i) {27.
aA[i] = 10;28.
aB[i] = 20;
29.
});
30.
});
31.
32.
for (int j = 0; j < STEPS; j++) {33.
Q.submit([&](auto &h) {34.
// デバイスアクセサーを作成
35.
sycl::accessor aA(ABuf, h);36.
sycl::accessor aB(BBuf, h);37.
sycl::accessor aC(CBuf, h);38.
h.parallel_for(N, [=](auto i) {39.
aC[i] = (aA[i] < aB[i]) ? -1 : 1;
40.
aA[i] += aC[i];41.
aB[i] -= aC[i];
42.
});
43.
});
44.
} // for の終わり
45.
46.
// ホストアクセサーを作成
47.
const sycl::host_accessor haA(ABuf);48.
const sycl::host_accessor haB(BBuf);49.
printf("%d %d\n", haA[N / 2], haB[N / 2]);50.
51.
return 0;
52. }5-11 バッファー・アクセサー・モード
SYCL* では、バッファーはホストまたはデバイスからアクセスできるメモリーへの抽象化されたビューを提供します。
バッファー・オブジェクトから直接バッファーにアクセスすることはできません。代わりに、バッファーデータにアクセスできるアクセサー・オブジェクトを作成します。
アクセスモードは、プログラム内でアクセサーに関連付けられたメモリーをどのように使用するかを示します。アクセ
サーのアクセスモードは、ランタイムがカーネルの実行順序を決定し、データの移動を行うために使用されます。これ
により、プログラマーが意図した順番でカーネルを実行できるようになります。依存関係違反や競合状態が発生しない場合、ベースとなるハードウェアの機能に応じて、ランタイムはカーネルを同時に実行できます。
パフォーマンスを向上させるには、アクセサーのアクセスモードが、カーネルによる実行操作に対応していることを確認します。read_only として宣言されているアクセサーに書き込みを行うと、コンパイル時にエラーとなります。しかし、カーネルが書き込みを行わない場合、コンパイラーは read_write 形式で宣言されたアクセサーを read に変更することはありません。
次の例には 3 つのカーネルがあります。最初のカーネルは ABuf、BBuf、および CBuf を初期化するため、バッファーのアクセスモードを write_only としています。2 番目のカーネルは ABuf と BBuf を読み取り、CBuf を読み書きするため、ABuf と BBuf のアクセスモードを read_only、そして CBuf のアクセスモードを read_write に指定
します。

156

oneAPI GPU 最適化ガイド
read_only アクセスモードは、カーネルが実行を開始する前にデータがデバイスで利用可能でなければならないことをランタイムに通知しますが、計算が完了してもデータをデバイスからホストにコピーする必要はありません。
2 番目のカーネルで ABuf と BBuf が read_only でなく read_write である場合、デバイスでデータが更新されていなくてもカーネルの終了時に ABuf と BBuf に関連するメモリーがデバイスからホストにコピーされます。
さらに、read_write にすると依存関係も生じるようになります。ABuf または BBuf を読み取る別のカーネルがこのブロック内で送信されると、新しいカーネルは 2 番目のカーネルが完了するまで実行を開始できません。
1. #include <CL/sycl.hpp>
2. #include <stdio.h>3.
4. constexpr int N = 100;
5.
6. int main() {7.
8.
int AData[N];9.
int BData[N];10.
int CData[N];11.
12.
sycl::queue Q;
13.
14.
// カーネル 1
15.
{16.
// それぞれ N 個の整数を保持する 3 つのバッファーを作成
17.
sycl::buffer<int> ABuf(&AData[0], N);18.
sycl::buffer<int> BBuf(&BData[0], N);
19.
sycl::buffer<int> CBuf(&CData[0], N);
20.
21.
Q.submit([&](auto &h) {22.
// デバイスアクセサーを作成。
23.

// プロパティー no_init は、バッファーの以前の

24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.

// 内容を破棄できることをランタイムに通知します。
sycl::accessor aA(ABuf, h, sycl::write_only, sycl::no_init);
sycl::accessor aB(BBuf, h, sycl::write_only, sycl::no_init);
sycl::accessor aC(CBuf, h, sycl::write_only, sycl::no_init);h.parallel_for(N, [=](auto i) {aA[i] = 11;aB[i] = 22;aC[i] = 0;});});
} // Kernel1 の終わり// カーネル 2{// それぞれ N 個の整数を保持する 3 つのバッファーを作成
sycl::buffer<int> ABuf(&AData[0], N);
sycl::buffer<int> BBuf(&BData[0], N);
sycl::buffer<int> CBuf(&CData[0], N);
Q.submit([&](auto &h) {// デバイスアクセサーを作成
sycl::accessor aA(ABuf, h, sycl::read_only);
sycl::accessor aB(BBuf, h, sycl::read_only);
sycl::accessor aC(CBuf, h);h.parallel_for(N, [=](auto i) { aC[i] += aA[i] + aB[i]; });});} // Kernel 2 の終わり// バッファーは破棄され、CData が更新されアクセスできるようになりました

157

oneAPI GPU 最適化ガイド
54.
for (int i = 0; i < N; i++) {55.
printf("%d\n", CData[i]);
56.
}
57.
58.
return 0;
59. }read_write アクセスモードの代わりに read_only を指定するのは、カーネルが for ループ内で繰り返し起動される場合に有用です。アクセスモードが read_write である場合、カーネルが計算を終了し次のカーネルを起動する前にデータの準備ができている必要があるため、起動されたカーネルはシリアル化されます。一方、アクセスモード
が read_only の場合、ランタイムはカーネルを並列に起動できます。
バッファーの宣言とカーネルの起動は、ブロック内にあることに注意してください。これにより、最初のカーネルが完了するとバッファーはスコープ外になります。そしてデバイスからホストへのコンテンツのコピーがトリガーされます。
2 番目のカーネルは、新しいバッファーが同じメモリーに宣言される別のブロック内にあるため、ホストからデバイスへの同一メモリーのコピーが再びトリガーされます。ホストとデバイス間の往復コピーは、バッファーを宣言してから、このバッファーが示すメモリーが存続する間にスコープ内にあることを確認することで回避できます。不要なメモリー転送を回避する方法を以下に示します。
1. #include <CL/sycl.hpp>
2. #include <stdio.h>3.
4. constexpr int N = 100;
5.
6. int main() {7.
8.
int AData[N];9.
int BData[N];10.
int CData[N];11.
12.
sycl::queue Q;
13.
14.
// それぞれ N 個の整数を保持する 3 つのバッファーを作成
15.
sycl::buffer<int> ABuf(&AData[0], N);16.
sycl::buffer<int> BBuf(&BData[0], N);17.
sycl::buffer<int> CBuf(&CData[0], N);18.
19.
// カーネル 1
20.
Q.submit([&](auto &h) {21.
// デバイスアクセサーを作成。
22.

// プロパティー no_init は、バッファーの以前の

23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.

// 内容を破棄できることをランタイムに通知します。
sycl::accessor aA(ABuf, h, sycl::write_only, sycl::no_init);
sycl::accessor aB(BBuf, h, sycl::write_only, sycl::no_init);
sycl::accessor aC(CBuf, h, sycl::write_only, sycl::no_init);

158

h.parallel_for(N, [=](auto i) {aA[i] = 11;aB[i] = 22;aC[i] = 0;});});// カーネル 2
Q.submit([&](auto &h) {// デバイス sycl::accessors を作成
sycl::accessor aA(ABuf, h, sycl::read_only);
sycl::accessor aB(BBuf, h, sycl::read_only);
sycl::accessor aC(CBuf, h);h.parallel_for(N, [=](auto i) { aC[i] += aA[i] + aB[i]; });});oneAPI GPU 最適化ガイド
43.
44.

// ホストアクセサーを作成することで、カーネルの終了待機がトリガーされ、

45.
// デバイスからホストへデータがコピーされます46.
sycl::host_accessor h_acc(CBuf);47.
for (int i = 0; i < N; i++) {48.
printf("%d\n", h_acc[i]);
49.
}
50.
51.
return 0;52. }次の例は、異なるスコープのブロック化による別の手法を示します。この例では、カーネル 1 の終わりとカーネル 2 の始めにホストからデバイスへのバッファーコピーはありません。これら 3 つのバッファーのコピーは、バッファーがス
コープ外になったカーネル 2 の最後で発生します。
1. #include <CL/sycl.hpp>
2. #include <stdio.h>3.
4. constexpr int N = 100;
5.
6. int main() {7.
8.
int AData[N];9.
int BData[N];10.
int CData[N];11.
12.
sycl::queue Q;
13.
14.
{
15.
// それぞれ N 個の整数を保持する 3 つのバッファーを作成
16.
sycl::buffer<int> ABuf(&AData[0], N);17.
sycl::buffer<int> BBuf(&BData[0], N);18.
sycl::buffer<int> CBuf(&CData[0], N);
19.
20.
// カーネル 1
21.
Q.submit([&](auto &h) {22.
// デバイスアクセサーを作成。
23.

// プロパティー no_init は、バッファーの以前の

24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.

// 内容を破棄できることをランタイムに通知します。
sycl::accessor aA(ABuf, h, sycl::write_only, sycl::no_init);
sycl::accessor aB(BBuf, h, sycl::write_only, sycl::no_init);
sycl::accessor aC(CBuf, h, sycl::write_only, sycl::no_init);}// バッファーはスコープ外となるため、デバイスからホストへコピーし戻す必要があります。

46.

// これには、すべてのカーネルが終了するまで待機する必要がありますが、

h.parallel_for(N, [=](auto i) {aA[i] = 11;aB[i] = 22;aC[i] = 0;});});// カーネル 2
Q.submit([&](auto &h) {// デバイスアクセサーを作成
sycl::accessor aA(ABuf, h, sycl::read_only);
sycl::accessor aB(BBuf, h, sycl::read_only);
sycl::accessor aC(CBuf, h);h.parallel_for(N, [=](auto i) { aC[i] += aA[i] + aB[i]; });
});

159

oneAPI GPU 最適化ガイド
47.
// 明示的な待機は必要ありません48.
for (int i = 0; i < N; i++) {49.
printf("%d\n", CData[i]);
50.
}
51.
52.
return 0;53. }以下に示すように、カーネルを定義するラムダ関数の変数キャプチャーの一部として、ホスト上の read-only 変数のコピーにデバイスがアクセスできるカーネルを作成する方法もあります。しかしこの方法では、カーネルの呼び出しごとにベクトル AData と BData に関連付けられたデータをデバイスにコピーする必要があります。
1. #include <CL/sycl.hpp>
2. #include <stdio.h>3.
4. constexpr int N = 100;
5. constexpr int iters = 100;
6.
7. int main() {8.
9.
int AData[N];10.
int BData[N];11.
int CData[N];12.
13.
sycl::queue Q;
14.
sycl::buffer<int> CBuf(&CData[0], N);
15.
16.
{
17.
// それぞれ N 個の整数を保持する 2 つのバッファーを作成
18.
sycl::buffer<int> ABuf(&AData[0], N);
19.
sycl::buffer<int> BBuf(&BData[0], N);
20.
21.
// カーネル 1
22.
Q.submit([&](auto &h) {23.
// デバイスアクセサーを作成。
24.

// プロパティー no_init は、バッファーの以前の

25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.

// 内容を破棄できることをランタイムに通知します。
sycl::accessor aA(ABuf, h, sycl::write_only, sycl::no_init);
sycl::accessor aB(BBuf, h, sycl::write_only, sycl::no_init);
sycl::accessor aC(CBuf, h, sycl::write_only, sycl::no_init);

160

h.parallel_for(N, [=](auto i) {aA[i] = 11;aB[i] = 22;aC[i] = 0;
});
});
}
for (int it = 0; it < iters; it++) {// カーネル 2
Q.submit([&](auto &h) {// デバイスアクセサーを作成
sycl::accessor aC(CBuf, h);h.parallel_for(N, [=](auto i) { aC[i] += AData[i] + BData[i]; });});}sycl::host_accessor h_acc(CBuf);for (int i = 0; i < N; i++) {printf("%d\n", h_acc[i]);}oneAPI GPU 最適化ガイド
52.
return 0;53. }ベクトルがホストからデバイスに一度だけコピーされるように、バッファーと read-only アクセサーを定義することを推奨します。次のカーネルでは、AData と Bdata へのアクセスは 24 行目と 25 行目の aA(ABuf) とaB(BBuf) を介して行われ、36 行目と 37 行目で read-only として定義されているため、スコープ外になってもデバイスからホストへのコピーは発生しません。
1. #include <CL/sycl.hpp>
2. #include <stdio.h>
3. constexpr int N = 100;
4. constexpr int iters = 100;
5. int main() {6.
int AData[N];7.
int BData[N];8.
int CData[N];9.
sycl::queue Q;
10.
sycl::buffer<int> CBuf(&CData[0], N);
11.
{
12.
// それぞれ N 個の整数を保持する 2 つのバッファーを作成
13.
sycl::buffer<int> ABuf(&AData[0], N);
14.
sycl::buffer<int> BBuf(&BData[0], N);
15.
// カーネル 1
16.
Q.submit([&](auto &h) {17.
// デバイスアクセサーを作成。
18.

// プロパティー no_init は、バッファーの以前の

19.
// 内容を破棄できることをランタイムに通知します。
20.
sycl::accessor aA(ABuf, h, sycl::write_only, sycl::no_init);
21.
sycl::accessor aB(BBuf, h, sycl::write_only, sycl::no_init);
22.
sycl::accessor aC(CBuf, h, sycl::write_only, sycl::no_init);
23.
h.parallel_for(N, [=](auto i) {24.
aA[i] = 11;25.
aB[i] = 22;26.
aC[i] = 0;
27.
});
28.
});
29.
}
30.
sycl::buffer<int> ABuf(&AData[0], N);31.
sycl::buffer<int> BBuf(&BData[0], N);32.
for (int it = 0; it < iters; it++) {33.
// カーネル 2
34.
Q.submit([&](auto &h) {35.
// デバイスアクセサーを作成
36.
sycl::accessor aA(ABuf, h, sycl::read_only);37.
sycl::accessor aB(BBuf, h, sycl::read_only);38.
sycl::accessor aC(CBuf, h);39.
h.parallel_for(N, [=](auto i) { aC[i] += aA[i] + aB[i]; });
40.
});
41.
}
42.
sycl::host_accessor h_acc(CBuf);43.
for (int i = 0; i < N; i++) {44.
printf("%d\n", h_acc[i]);
45.
}
46.
return 0;
47. }

161

oneAPI GPU 最適化ガイド

5-12 ホスト/デバイスの調整
ホストとアクセラレーター・デバイス間には大量の計算および通信リソースがあるため、それらを効率良く利用することを考慮しなければなりません。
ここでは、ホストとアクセラレーターの処理の調整に関連するトピックについて説明します。
•

ホストとデバイス間のデバイス間の非同期および重複データ転送

5-12-1

ホストとデバイス間のデバイス間の非同期および重複データ転送

アクセラレーターはホスト CPU とは別のデバイスであり、PCIe* や CXL* などのバスに接続されます。このバスは、そのタイプに応じてホストとデバイス間で転送できるデータ帯域幅が定められます。アクセラレーターは計算に必要なデータをホストから転送しなければならず、システム全体のパフォーマンスは、この転送がどれだけ高速に行われる
かに依存します。

5-12-1-1

ホストとデバイス間の帯域幅

ほとんどのアクセラレーターは、PCIe* を介してホストと接続されます。次の表に示すように、PCIe* の世代が異なると帯域幅の性能は異なります。
世代ごとの PCIe* 帯域幅
PCIe* バージョン
1.0
2.0
3.0
4.0
5.0

転送レート
2.5GT/秒
5.0GT/秒
8.0GT/秒
16.0GT/秒
32.0GT/秒

スループット0.250GB/秒
0.500GB/秒
0.985GB/秒
1.969GB/秒
3.938GB/秒

アクセラレーターが搭載するローカルメモリーの帯域幅は、PCIe* リンクを介したホストからデバイスへの帯域幅よりも 1 桁高くなります。例えば、最新 GPU の HBM (High Bandwidth Memory) は、x16 PCIe* の 63GB/秒の帯域幅と比べると、最大 900GB/秒にも達します。そのため、データをローカルメモリーに保持して、ホストとデバイス間のデー
タ転送を可能な限り回避することが不可欠です。例えば、一部のカーネルがアクセラレーターで効率良く実行できない場合でも、アクセラレーター間、またはホストとアクセラレーター間でのデータ移動を回避するため、アクセラレー
ターですべてのカーネルを実行するほうが最終的に良い結果となることもあります。
また、中間データ構造を使用する場合、ホストでデータを作成してアクセラレーター間で移動を行うのではなく、デバ
イス上でデータを作成して使用します。ここでは、デバイス上で中間結果を生成してリダクション操作を行うカーネルについて考えます。カーネル ComputeParallel1 では、一時アキュムレーターがホスト上に作成され、すべてのwork-item が中間結果をホストに配置します。このアキュムレーターはホストに戻され、さらにレデュースされます(37 行目)。
1.
2.
3.
4.
5.
6.
7.
8.
9.

162

float ComputeParallel1(sycl::queue &q, std::vector<float> &data) {const size_t data_size = data.size();
float sum = 0;
static float *accum = 0;
if (data_size > 0) {const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};int num_EUs =q.get_device().get_info<sycl::info::device::max_compute_units>();oneAPI GPU 最適化ガイド
10.
int vec_size =11.
q.get_device()12.
.get_info<sycl::info::device::native_vector_width_float>();13.
int num_processing_elements = num_EUs * vec_size;14.
int BATCH = (N + num_processing_elements - 1) / num_processing_elements;
15.
sycl::buffer<float> buf(data.data(), data.size(), props);
16.
sycl::buffer<float> accum_buf(accum, num_processing_elements, props);
17.
18.
if (!accum)
19.
accum = new float[num_processing_elements];
20.
21.
q.submit([&](auto &h) {22.
sycl::accessor buf_acc(buf, h, sycl::read_only);23.
sycl::accessor accum_acc(accum_buf, h, sycl::write_only, sycl::no_init);
24.
h.parallel_for(num_processing_elements, [=](auto index) {25.
size_t glob_id = index[0];26.
size_t start = glob_id * BATCH;
27.
size_t end = (glob_id + 1) * BATCH;28.
if (end > N)29.
end = N;30.
float sum = 0.0;31.
for (size_t i = start; i < end; i++)32.
sum += buf_acc[i];33.
accum_acc[glob_id] = sum;
34.
});
35.
});
36.
q.wait();37.
sycl::host_accessor h_acc(accum_buf);38.
for (int i = 0; i < num_processing_elements; i++)39.
sum += h_acc[i];
40.
}
41.
return sum;42. } // ComputeParallel1 の終わり

別のアプローチとして、この一時アキュムレーターをアクセラレーターに保持して、1 つの work-item のみで別のカーネルを起動する方法があります。ここでは、ComputeParallel2 カーネルの 43 行目でデバイスが最終的なリダクション操作を実行します。このカーネルにはそれほど多くの並列処理がないため、1 つの work-item によってのみ実
行されることに注意してください。一部のプラットフォームでは、これはデータをホストに転送してからホストでリダク
ションを行うより良好な場合があります。
1. float ComputeParallel2(sycl::queue &q, std::vector<float> &data) {2.
const size_t data_size = data.size();
3.
float sum = 0;
4.
static float *accum = 0;
5.
6.
if (data_size > 0) {7.
const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};8.
int num_EUs =9.
q.get_device().get_info<sycl::info::device::max_compute_units>();10.
int vec_size =11.
q.get_device()12.
.get_info<sycl::info::device::native_vector_width_float>();13.
int num_processing_elements = num_EUs * vec_size;14.
int BATCH = (N + num_processing_elements - 1) / num_processing_elements;
15.
sycl::buffer<float> buf(data.data(), data.size(), props);
16.
sycl::buffer<float> accum_buf(accum, num_processing_elements, props);
17.
sycl::buffer<float> res_buf(&sum, 1, props);
18.
if (!accum)
19.
accum = new float[num_processing_elements];
20.
21.
q.submit([&](auto &h) {22.
sycl::accessor buf_acc(buf, h, sycl::read_only);23.
sycl::accessor accum_acc(accum_buf, h, sycl::write_only, sycl::no_init);
24.
h.parallel_for(num_processing_elements, [=](auto index) {25.
size_t glob_id = index[0];26.
size_t start = glob_id * BATCH;

163

oneAPI GPU 最適化ガイド
27.
size_t end = (glob_id + 1) * BATCH;28.
if (end > N)29.
end = N;30.
float sum = 0.0;31.
for (size_t i = start; i < end; i++)32.
sum += buf_acc[i];33.
accum_acc[glob_id] = sum;
34.
});
35.
});
36.
37.
q.submit([&](auto &h) {38.
sycl::accessor accum_acc(accum, h, sycl::read_only);39.
sycl::accessor res_acc(res_buf, h, sycl::write_only, sycl::no_init);
40.
h.parallel_for(1, [=](auto index) {41.
res_acc[index] = 0;42.
for (size_t i = 0; i < num_processing_elements; i++)43.
res_acc[index] += accum_acc[i]; // 最終的なリダクション
44.
});
45.
});
46.
}
47.
// バッファーがスコープ外となるため、データはデバイスからホストへ転送されます48.
return sum;49. } // ComputeParallel2 の終わり

5-12-1-2

デバイスでの計算とホスト/デバイス間のオーバーラップ・データ転送

一部の GPU には、ホストからデバイスにデータをコピーする専用エンジンを持つものがあります。それらを効果的に利用することで、ホストからデバイスへのデータ転送をデバイスの実行とオーバーラップさせることができます。次の例では、メモリーブロックがいくつかのチャンクに分割され、それぞれのチャンクがアクセラレーターに転送されて(49 行目)、処理され (53 行目)、結果がホストに戻されます (54 行目)。この 3 つのタスクのチャンクは独立しているため、ハードウェア・リソースの状況に応じて並列に処理できます。ホストとデバイス間のデータ転送に利用できるコ
ピーエンジンが存在するシステムでは、異なるループ反復から操作を並列に実行できることが分かります。次の 2 つの方法で並列実行できます。
•

1 つは GPU ベクトルエンジン で実行され、もう 1 つはコピーエンジンによって実行される 2 つのメモリーコ
ピー間
コピーはコピーエンジンによって実行され、計算カーネルは GPU ベクトルエンジン で実行される、メモリーコ
ピーと計算カーネル間•

1. #include <CL/sycl.hpp>
2. #define NITERS 10
3. #define KERNEL_ITERS 10000
4. #define NUM_CHUNKS 10
5. #define CHUNK_SIZE 10000000
6. class Timer {7.
public:8.
Timer() : start_(std::chrono::steady_clock::now()) {}9.
double Elapsed() {10.
auto now = std::chrono::steady_clock::now();
11.
return std::chrono::duration_cast<Duration>(now - start_).count();
12.
}
13.
private:14.
using Duration = std::chrono::duration<double>;15.
std::chrono::steady_clock::time_point start_;
16. };
17. int main() {18.
const int num_chunks = NUM_CHUNKS;
19.
const int chunk_size = CHUNK_SIZE;
20.
const int iter = NITERS;
21.
sycl::queue q;
22.
// ホストデータの割り当てと初期化

164

oneAPI GPU 最適化ガイド
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72. }

float *host_data[num_chunks];
for (int c = 0; c < num_chunks; c++) {host_data[c] = sycl::malloc_host<float>(chunk_size, q);
float val = c;for (int i = 0; i < chunk_size; i++)host_data[c][i] = val;}
std::cout << "Allocated host data\n";// デバイスメモリーに配列を割り当てて初期化
float *device_data[num_chunks];
for (int c = 0; c < num_chunks; c++) {device_data[c] = sycl::malloc_device<float>(chunk_size, q);
float val = 1000.0;
q.fill<float>(device_data[c], val, chunk_size);}q.wait();std::cout << "Allocated device data\n";Timer timer;for (int it = 0; it < iter; it++) {for (int c = 0; c < num_chunks; c++) {auto add_one = [=](auto id) {for (int i = 0; i < KERNEL_ITERS; i++)device_data[c][id] += 1.0;};// 以前のイベントに依存しないコピーイン
auto copy_in =q.memcpy(device_data[c], host_data[c], sizeof(float) * chunk_size);// 計算はコピーインを待機
auto compute = q.parallel_for(chunk_size, copy_in, add_one);
auto cg = [=](auto &h) {h.depends_on(compute);h.memcpy(host_data[c], device_data[c], sizeof(float) * chunk_size);};// コピーアウトは計算を待機
auto copy_out = q.submit(cg);}q.wait();}auto elapsed = timer.Elapsed() / iter;for (int c = 0; c < num_chunks; c++) {for (int i = 0; i < chunk_size; i++) {if (host_data[c][i] != (float)((c + KERNEL_ITERS * iter))) {std::cout << "Mismatch for chunk: " << c << " position: " << i<< " expected: " << c + 10000 << " got: " << host_data[c][i]<< "\n";break;}}}
std::cout << "Time = " << elapsed << " usecs\n";onetrace で収集された次の図のタイムラインでは、コピーインが計算カーネルの実行とオーバーラップしていることが分かります。また、複数のコピーインが複数のコピーエンジンで並列に実行されていることが分かります。

165

oneAPI GPU 最適化ガイド
計算カーネルの実行とコピーインのオーバーラップ

上記の例では、GPU が 1 つしかないため (独立していても) 2 つのカーネルを同時に実行することはできません (GPUを小さなチャンクに分割して、それらで異なるカーネルを同時に実行することは可能です)。

5-13 複数のヘテロジニアス・デバイスを使用
通常ほとんどのアクセラレーターは、大量の計算リソースを持つサーバーに搭載されます。例えば、一般的なサーバーには最大 8 つのソケットがあり、それぞれのソケットには 50 以上のコアがあります。SYCL* は、CPU とアクセラレー
ターを均一に扱い、それらにワークを分散する機能を提供します。プラットフォーム上のヘテロジニアス・コンピュー
ティングのリソース間にワークをバランスよく分散するのは、プログラマーの責任です。

5-13-1

各種デバイスで計算をオーバーラップ

SYCL* は、デバイスセレクターの抽象化によりさまざまなデバイスへのアクセスを可能にします。デバイスごとにキューを作成して、カーネルをデバイスに送信し実行することができます。SYCL* におけるすべてのカーネル送信は非ブロッキングです。つまり、実行のためカーネルがキューに送信されると、キューの待機が明示的に要求されない限
り、ホストはカーネルの終了を待機しません。これにより、カーネルがアクセラレーターで実行されている間に、ホストはワークを実行したり、他のデバイスでワークを開始することができます。
ホスト CPU はアクセラレーターとして扱うことができ、SYCL* はカーネルをホスト CPU に送信して実行することができます。これは完全に独立しており、カーネルの送信と作成を調整するためホストが実行するジョブと直交しています。ベースとなるオペレーティング・システムは、CPU アクセラレーターに送信されたカーネルを別のプロセスとして管理し、同じ OpenCL*/レベル 0 ランタイムの機能を使用してホストデバイスと情報を交換します。
以下では、単一の GPU デバイスで機能する単純なベクトル加算の例を示します。
1. size_t VectorAdd1(sycl::queue &q, const IntArray &a, const IntArray &b,
2.
IntArray &sum, int iter) {3.
sycl::range num_items{a.size()};4.
5.
sycl::buffer a_buf(a);6.
sycl::buffer b_buf(b);7.
sycl::buffer sum_buf(sum.data(), num_items);
8.
auto start = std::chrono::steady_clock::now();9.
for (int i = 0; i < iter; i++) {10.
11.
auto e = q.submit([&](auto &h) {12.
// 入力アクセサー
13.
sycl::accessor a_acc(a_buf, h, sycl::read_only);14.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
15.
// 出力アクセサー

166

oneAPI GPU 最適化ガイド
16.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
17.
18.
h.parallel_for(num_items,
19.
[=](auto i) { sum_acc[i] = a_acc[i] + b_acc[i]; });
20.
});
21.
}
22.
q.wait();23.
auto end = std::chrono::steady_clock::now();24.
std::cout << "Vector add1 completed on device - took "25.
<< (end - start).count() << " u-secs\n";26.
return ((end - start).count());
27. } // VectorAdd1 の終わり

次のカーネルでは、入力ベクトルが 2 つに分割され、同時に実行可能な 2 つの異なるアクセラレーター (CPU とGPU) で計算されます。この並列処理では、送信されるだけではなく、カーネルが実際にデバイスで起動されるようにする必要があります。カーネルの起動に必要な時間は、ホストから送信される場合よりも大幅に長くなる可能性があります。利用率、スループット、待機時間などのメトリックを最大化するため、実装のヒューリスティックに基づいてカーネルを起動する時間を決定します。例えば、OpenCL* バックエンドの特定のプラットフォームでは、キューでclFlush(cq); を明示的に発行してから (41 行目)、アクセラレーターでカーネルを起動する必要があります。
1. size_t VectorAdd2(sycl::queue &q1, sycl::queue &q2, const IntArray &a,
2.
const IntArray &b, IntArray &sum, int iter) {3.
sycl::range num_items{a.size() / 2};
4.
5.
auto start = std::chrono::steady_clock::now();6.
{
7.
sycl::buffer a1_buf(a.data(), num_items);8.
sycl::buffer b1_buf(b.data(), num_items);
9.
sycl::buffer sum1_buf(sum.data(), num_items);
10.
11.
sycl::buffer a2_buf(a.data() + a.size() / 2, num_items);
12.
sycl::buffer b2_buf(b.data() + a.size() / 2, num_items);
13.
sycl::buffer sum2_buf(sum.data() + a.size() / 2, num_items);14.
for (int i = 0; i < iter; i++) {15.
16.
q1.submit([&](auto &h) {17.
// 入力アクセサー
18.
sycl::accessor a_acc(a1_buf, h, sycl::read_only);19.
sycl::accessor b_acc(b1_buf, h, sycl::read_only);
20.
// 出力アクセサー
21.
sycl::accessor sum_acc(sum1_buf, h, sycl::write_only, sycl::no_init);
22.
23.
h.parallel_for(num_items,
24.
[=](auto i) { sum_acc[i] = a_acc[i] + b_acc[i]; });
25.
});
26.
// ホストでワークを実行
27.
q2.submit([&](auto &h) {28.
// 入力アクセサー
29.
sycl::accessor a_acc(a2_buf, h, sycl::read_only);30.
sycl::accessor b_acc(b2_buf, h, sycl::read_only);
31.
// 出力アクセサー
32.
sycl::accessor sum_acc(sum2_buf, h, sycl::write_only, sycl::no_init);
33.
34.
h.parallel_for(num_items,
35.
[=](auto i) { sum_acc[i] = a_acc[i] + b_acc[i]; });
36.
});
37.
}
38.
// 一部のプラットフォームでは、CPU と GPU 間の実行がオーバーラップすることを
39.
40.
41.
42.

// 確実にするため、明示的なキューのフラッシュが必要になります// cl_command_queue cq = q1.get();// clFlush(cq);// cq=q2.get();167

oneAPI GPU 最適化ガイド
43.
// clFlush(cq);
44.
}
45.
q1.wait();46.
q2.wait();47.
auto end = std::chrono::steady_clock::now();48.
std::cout << "Vector add2 completed on device - took "49.
<< (end - start).count() << " u-secs\n";50.
return ((end - start).count());
51. } // VectorAdd2 の終わり

上記 2 つのカーネルの実行時間を確認すると、問題解決に利用できる専用ハードウェアがあるため、アプリケーショ
ンは以前のおよそ 2 倍高速に実行されていることが分かります。良好なバランスを達成するため、上記の例のようにワークを均等に分割する代わりに、アクセラレーターの機能に応じて分割することを考えなければなりません。

5-14 コンパイル
oneAPI には、異なる複数のコンパイル方法があります。アプリケーションの main のソースがコンパイルされ、オフロードされるカーネルがコンパイルされます。カーネルは、事前 (AOT) またはジャストイン (JIT) コンパイルされる場
合があります。
ここでは、コンパイルに関連するトピックと、実行効率に与える影響について説明します。
••••

ジャストインタイム・コンパイル
事前コンパイル
特殊化定数
浮動小数点計算における精度とパフォーマンスのトレードオフ

5-14-1

ジャストインタイム・コンパイルインテル® oneAPI の SYCL* コンパイラーは、SYCL* プログラムを SPIR-V* と呼ばれる中間言語に変換し、コンパイ
ルで生成されるバイナリー内に格納します。バイナリーの代わりに中間ファイルを生成する利点は、実行時に SPIRV* コードをプラットフォームのバイナリーコードに変換することで、このコードを任意のハードウェア・プラットフォー
ムで実行できることです。中間コードを実行時にバイナリーに変換するプロセスは、JIT (ジャストインタイム) コンパイ
ルと呼ばれます。JIT コンパイルは実行時に要求に応じて行われます。この JIT コンパイルを制御するにはいくつかの方法があります。デフォルトでは、バイナリー内の SPIR-V* コードは、最初にオフロードされたカーネルの実行開始前に変換されます。
1. #include <CL/sycl.hpp>
2. #include <array>
3. #include <chrono>
4. #include <iostream>
5.
6. // この例の配列タイプとデータサイズ
7. constexpr size_t array_size = (1 << 16);
8. typedef std::array<int, array_size> IntArray;
9.
10. void VectorAdd1(sycl::queue &q, const IntArray &a, const IntArray &b,
11.
IntArray &sum) {12.
sycl::range num_items{a.size()};13.
14.
sycl::buffer a_buf(a);15.
sycl::buffer b_buf(b);16.
sycl::buffer sum_buf(sum.data(), num_items);
17.
18.
auto e = q.submit([&](auto &h) {168

oneAPI GPU 最適化ガイド
19.
// 入力アクセサー
20.
sycl::accessor a_acc(a_buf, h, sycl::read_only);21.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
22.
// 出力アクセサー
23.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
24.
25.
h.parallel_for(num_items,
26.
[=](auto i) { sum_acc[i] = a_acc[i] + b_acc[i]; });
27.
});
28.
q.wait();29. }30.
31. void VectorAdd2(sycl::queue &q, const IntArray &a, const IntArray &b,
32.
IntArray &sum) {33.
sycl::range num_items{a.size()};34.
35.
sycl::buffer a_buf(a);36.
sycl::buffer b_buf(b);37.
sycl::buffer sum_buf(sum.data(), num_items);
38.
39.
auto e = q.submit([&](auto &h) {40.
// 入力アクセサー
41.
sycl::accessor a_acc(a_buf, h, sycl::read_only);42.
sycl::accessor b_acc(b_buf, h, sycl::read_only);
43.
// 出力アクセサー
44.
sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);
45.
46.
h.parallel_for(num_items,
47.
[=](auto i) { sum_acc[i] = a_acc[i] + b_acc[i]; });
48.
});
49.
q.wait();50. }51.
52. void InitializeArray(IntArray &a) {53.
for (size_t i = 0; i < a.size(); i++)54.
a[i] = i;
55. }56.
57. int main() {58.
IntArray a, b, sum;
59.
60.
InitializeArray(a);61.
InitializeArray(b);62.
63.
sycl::queue q(sycl::default_selector_v,
64.
sycl::property::queue::enable_profiling{});65.
66.
std::cout << "Running on device: "67.
<< q.get_device().get_info<sycl::info::device::name>() << "\n";68.
std::cout << "Vector size: " << a.size() << "\n";69.
auto start = std::chrono::steady_clock::now();70.
VectorAdd1(q, a, b, sum);71.
auto end = std::chrono::steady_clock::now();72.
std::cout << "Initial Vector add1 successfully completed on device - took "73.
<< (end - start).count() << " nano-secs\n";74.
75.
start = std::chrono::steady_clock::now();76.
VectorAdd1(q, a, b, sum);77.
end = std::chrono::steady_clock::now();78.
std::cout << "Second Vector add1 successfully completed on device - took "79.
<< (end - start).count() << " nano-secs\n";80.
81.
start = std::chrono::steady_clock::now();82.
VectorAdd2(q, a, b, sum);83.
end = std::chrono::steady_clock::now();84.
std::cout << "Initial Vector add2 successfully completed on device - took "85.
<< (end - start).count() << " nano-secs\n";169

oneAPI GPU 最適化ガイド
86.
87.
start = std::chrono::steady_clock::now();
88.
VectorAdd2(q, a, b, sum);89.
end = std::chrono::steady_clock::now();90.
std::cout << "Second Vector add2 successfully completed on device - took "91.
<< (end - start).count() << " nano-secs\n";92.
return 0;
93. }上記のプログラムを次のコマンドでコンパイルします (ソースファイル名は example.cpp)。
$ icpx -fsycl -O3 -o example example.cpp

実行すると、生成される出力から VectorAdd1 の最初の呼び出しは JIT コンパイルのコストが加算されるため、プログラム内のほかの VectorAdd1 カーネルの呼び出しよりもはるかに時間がかかることが分かります。
アプリケーションに複数のカーネルが含まれる場合、コンパイル時にオプションを指定して、eager または lazy JIT
コンパイルを強制できます。eager JIT コンパイルでは、実行時にバイナリー内のすべてのカーネルが JIT を呼び出しますが、lazy JIT コンパイルでは、実行中に実際にカーネルが呼び出されたときにのみ JIT を有効にします。一部のカーネルが呼び出されない状況では、実行されないコードを変換しないという利点があり、不要な JIT コンパイルを回避できます。このモードは、次のオプションで有効にできます。
-fsycl-device-code-split=<value>

ここで、value は次のいずれかです。
••••

per_kernel: カーネルが呼び出されたときにのみ JIT コンパイルを行うコードを生成します。
per_source: ソースファイル内のいずれかのカーネルが呼び出されたときに、ソースファイル内のすべてのカーネルを JIT コンパイルするコードを生成します。
off: アプリケーション内のすべてのカーネルを eager JIT コンパイルします。
auto: デフォルト。コンパイラーはヒューリスティックに従って JIT コンパイル用にデバイスコードを分割する最適な方法を選択します。

上記のプログラムを次のオプションでコンパイルします。
$ icpx -fsycl -O3 -o example vec1.cpp vec2.cpp main.cpp -fsycl-device-codesplit=per_kernel

プログラムを実行すると、カーネル実行のタイミングから、VectorAdd1 と VectorAdd2 の最初の起動まで時間がかかりますが、2 回目の呼び出しには JIT コンパイルのコストがないため、時間がかかっていないことが分かります。
上記の例では、VectorAdd1 と VectorAdd2 を異なるファイルに配置して per_source オプションを指定 (または未指定) して、カーネルの実行時間への影響を確認できます。次のようにコンパイルします。
$ icpx -fsycl -O3 -o example vec1.cpp vec2.cpp main.cpp -fsycl-device-codesplit=per_source

生成されたバイナリーを実行すると、カーネルの実行時間は、最初のカーネル呼び出しでは JIT コンパイルのコストが生じているのに対し、後続のカーネル呼び出しではそのコストがないことが分かります。次に、以下のようにコンパ
イルします。
$ icpx -fsycl -O3 -o example vec1.cpp vec2.cpp main.cpp

170

oneAPI GPU 最適化ガイド生成されたバイナリーを実行すると、カーネルの実行時間は、最初のカーネル呼び出しですべての JIT コンパイルのコストが発生し、後続のすべてのカーネル呼び出しでそのコストがないことが分かります。

5-14-2

事前コンパイル

実行時の JIT コンパイルのオーバーヘッドは、事前 (AOT) コンパイルによって回避できます。AOT コンパイルでは、
SPIR-V* 中間コードに代わって、コンパイル時に選択されたプラットフォームの実際のアセンブリー・コードがバイナリーに出力されます。AOT コンパイルの利点は、実行中に SPIR-V* からアセンブリー・コードに JIT コンパイルする必要がないため、コードの実行が高速化されることです。また欠点は、バイナリーは特定のプラットフォーム専用であるため、それ以外のプラットフォームでは実行できないことです。
インテル® GPU 向けに事前 (AOT) モードでコンパイルを行う場合、特定の GPU ターゲットを明確にするため追加のコンパイルオプション (-Xs) が必要です。

5-14-2-1

SYCL* 向けの AOT コンパイラー・オプション

インテル® データセンター GPU マックス・シリーズ:$ icpx -fsycl -fsycl-targets=spir64_gen -Xs "-device pvc" source.c

5-14-2-2

OpenMP* 向けの AOT コンパイラー・オプション

インテル® データセンター GPU マックス・シリーズ:$ icx -fiopenmp -fopenmp-targets=spir64_gen -Xs "-device pvc" source.c

注:••上記のコンパイルオプションは、OpenMP* Fortran プログラムを AOT モード (ifx を使用) でコンパイルする場合にも利用できます。
JIT モードでは、インテル® グラフィックス・コンパイラーがハードウェアのタイプを認識して、自動的に調整されます。-revision_id オプションは、AOT モードでのみ必要です。

5-14-3

特殊化定数

SYCL* には、特殊化定数と呼ばれる機能があり、JIT コンパイルを明示的にトリガーして、特殊化定数の実行時の値をベースに SPIR-V 中間コードからバイナリーを生成できます。この JIT コンパイルは、定数値が判明していればプログラムの実行中に行われます。これは、-fsycl-device-code-split オプションによってトリガーされる JIT コン
パイルとは異なります。
次の例では、set_specialization_constant 呼び出しにより、7 行目で定義される get_value 関数呼び出しで返される値が SYCL* カーネルにバインドされます。カーネルバンドルが最初にコンパイルされるときにこの値が不明であるため、最適化に利用できません。実行時に、get_value 関数が呼び出されると、値は既知となり コマンド・
グループ・ハンドラーで使用され、この値を基に特殊化されたカーネルの JIT コンパイルがトリガーされます。
1.
2.
3.
4.
5.

#include <CL/sycl.hpp>#include <vector>
class specialized_kernel;// 特殊化定数の const static 識別子
const static sycl::specialization_id<float> value_id;171oneAPI GPU 最適化ガイド
6. //ランタイムで値をフェッチ
7. float get_value() { return 10; };8. int main() {9.
sycl::queue queue;10.
std::vector<float> vec(1);
11.
{
12.
sycl::buffer<float> buffer(vec.data(), vec.size());
13.
queue.submit([&](auto &cgh) {14.
sycl::accessor acc(buffer, cgh, sycl::write_only, sycl::no_init);
15.
// 特殊化定数値を設定
16.
cgh.template set_specialization_constant<value_id>(get_value());
17.
// ランタイムは、特殊化定数を前の18.

// `set_specialization_constant<value_id>` の呼び出しで

19.
// 与えられたリテラル値に置き換えてカーネルを構築します20.
cgh.template single_task<specialized_kernel>(
21.
[=](sycl::kernel_handler kh) {22.
const float val = kh.get_specialization_constant<value_id>();23.
acc[0] = val;
24.
});
25.
});
26.
}
27.
queue.wait_and_throw();28.
std::cout << vec[0] << std::endl;
29.
return 0;
30. }22 行目の専用カーネルは最終的に次のようなコードになります。
cgh.single_task<specialized_kernel>( [=]() { acc[0] = 10; });この JIT コンパイルは、カーネルの実行時間にも影響します。以下に例を示します。
1. #include <CL/sycl.hpp>
2. #include <chrono>
3. #include <vector>
4.
5. class specialized_kernel;
6. class literal_kernel;
7.
8. // 特殊化定数の const static 識別子
9. const static sycl::specialization_id<float> value_id;
10.
11. //
ランタイムで値をフェッチ
12. float get_value() { return 10; };13.
14. int main() {15.
sycl::queue queue;
16.
17.
// カーネルクラス修飾子からカーネル ID を取得
18.
sycl::kernel_id specialized_kernel_id =19.
sycl::get_kernel_id<specialized_kernel>();20.
21.
// 入力状態の specialized_kernel のみでカーネルハンドルを構築
22.
sycl::kernel_bundle kb_src =23.
sycl::get_kernel_bundle<sycl::bundle_state::input>(
24.
queue.get_context(), {specialized_kernel_id});25.
// 特殊化定数値を設定
26.
kb_src.set_specialization_constant<value_id>(get_value());
27.
28.
auto start = std::chrono::steady_clock::now();
29.
// 設定値のカーネルハンドルを構築
30.
sycl::kernel_bundle kb_exe = sycl::build(kb_src);

172

oneAPI GPU 最適化ガイド
31.
auto end = std::chrono::steady_clock::now();32.
std::cout << "specialization took - " << (end - start).count()33.
<< " nano-secs\n";34.
35.
std::vector<float> vec{0, 0, 0, 0, 0};36.
sycl::buffer<float> buffer1(vec.data(), vec.size());
37.
sycl::buffer<float> buffer2(vec.data(), vec.size());
38.
start = std::chrono::steady_clock::now();
39.
{
40.
queue.submit([&](auto &cgh) {41.
sycl::accessor acc(buffer1, cgh, sycl::write_only, sycl::no_init);
42.
43.
// 事前コンパイルされたカーネルバンドルを実行状態で使用
44.
cgh.use_kernel_bundle(kb_exe);
45.
46.
cgh.template single_task<specialized_kernel>(
47.
[=](sycl::kernel_handler kh) {48.
float v = kh.get_specialization_constant<value_id>();49.
acc[0] = v;
50.
});
51.
});
52.
queue.wait_and_throw();
53.
}
54.
end = std::chrono::steady_clock::now();
55.
56.
{
57.
sycl::host_accessor host_acc(buffer1, sycl::read_only);58.
std::cout << "result1 (c): " << host_acc[0] << " " << host_acc[1] << " "59.
<< host_acc[2] << " " << host_acc[3] << " " << host_acc[4]60.
<< std::endl;
61.
}
62.
std::cout << "execution took : " << (end - start).count() << " nano-secs\n";63.
64.
start = std::chrono::steady_clock::now();
65.
{
66.
queue.submit([&](auto &cgh) {67.
sycl::accessor acc(buffer2, cgh, sycl::write_only, sycl::no_init);
68.
cgh.template single_task<literal_kernel>([=]() { acc[0] = 20; });
69.
});
70.
queue.wait_and_throw();
71.
}
72.
end = std::chrono::steady_clock::now();
73.
74.
{
75.
sycl::host_accessor host_acc(buffer2, sycl::read_only);76.
std::cout << "result2 (c): " << host_acc[0] << " " << host_acc[1] << " "77.
<< host_acc[2] << " " << host_acc[3] << " " << host_acc[4]78.
<< std::endl;
79.
}
80.
std::cout << "execution took - " << (end - start).count() << " nano-secs\n";81. }タイミングメッセージでレポートされるランタイムの情報を見ると、カーネルの初期変換に時間を要し、JIT コンパイ
ルされたカーネルの実行時間は短いことが分かります。特殊化定数なしで実行される同じカーネルは、実行される前にランタイムによって JIT コンパイルされるため、時間は長くなります。
特殊化定数の簡単な使用例とアプリケーションを以下に示します。

5-14-3-1

簡単なトリップカウントの例

次の例は、合計を実行し、特殊化定数によってトリップカウントを設定します。
1.
2.

#include <CL/sycl.hpp>

173

oneAPI GPU 最適化ガイド
3. class SpecializedKernel;
4.
5. // 特殊化定数を特定
6. constexpr sycl::specialization_id<int> nx_sc;
7.
8. int main() {9.
sycl::queue queue;
10.
11.
std::cout << "Running on "12.
<< queue.get_device().get_info<sycl::info::device::name>() << "\n";13.
14.
std::vector<float> vec(1);
15.
{
16.
sycl::buffer<float> buf(vec.data(), vec.size());
17.
18.
// アプリケーションはここで実行を停止し、ユーザーの入力を求めます19.
int nx;20.
std::cout << "Enter input number ..." << std::endl;
21.
std::cin >> Nx;
22.
23.
queue.submit([&](sycl::handler &h) {24.
sycl::accessor acc(buf, h, sycl::write_only, sycl::no_init);
25.
26.
// ランタイム変数で特殊化定数を設定
27.
h.set_specialization_constant<nx_sc>(Nx);28.
29.
h.single_task<SpecializedKernel>([=](sycl::kernel_handler kh) {30.
// ここでの nx_sc 値は、実行時に提供される入力値であり、
31.
// JIT コンパイラーが定数として使用できるため最適化可能です
32.
int runtime_const_trip_count = kh.get_specialization_constant<nx_sc>();33.
int accum = 0;34.
for (int i = 0; i < runtime_const_trip_count; i++) {35.
accum = accum + i;
36.
}
37.
acc[0] = accum;
38.
});
39.
});
40.
}
41.
std::cout << vec[0] << std::endl;
42.
return 0;
43. }ここでの目標は、カーネル内のループのトリップカウント変数 Nx を特殊化することです。プログラムの実行開始時にユーザーがトリップカウントを入力するため、ホスト・コンパイラーは Nx の値を認識できません。入力を特殊化定数として JIT コンパイラーに渡すことができるため、JIT コンパイラーはループアンロールなどの最適化を行うことができます。
特殊化定数を使用しない場合、同等の最適化を行うには変数 Nx はプログラム全体の定数式である必要があります。
特殊化定数によってランタイム変数から定数値を取得することで、最適化が可能となり、カーネルコードを高速化できます。
ホスト・コンパイラーはは、トリップカウントが定数ではない以下のループの例では、効果的な最適化が行えません。
for (int i = 0; i < Nx; i++) {// Nx が定数でない場合、最適化は制限されます}

174

oneAPI GPU 最適化ガイド

5-14-3-2

STREAM Triad アプリケーションを変更

次の例 (STREAM Triad の変更例) では、入力変数によって乗算器と乗算・加算の回数が決定される、いくつかの乗算・加算操作を持つ古典的な STREAM Triad を示します。
この例は、デバイスで実行されるオリジナルコードの一部です。ランタイム変数 inner_loop_size は、ループの上限を設定します。
1. auto q0_event = q.submit([&](sycl::handler &h) {2.
h.parallel_for<non_specialized_kernel>(array_size / 2, [=](auto idx) {3.
// トリップカウントをランタイム変数に設定
4.
auto runtime_trip_count_const = inner_loop_size;
5.
auto accum = 0;
6.
for (size_t j = 0; j < runtime_trip_count_const; j++) {7.
auto multiplier = scalar * j;8.
accum = accum + A0[idx] + B0[idx] * multiplier;
9.
}10.
C0[idx] = accum;
11.
});
12.
});
13.
14.
q.wait();15.
16.
cl_ulong exec_time_ns0 =17.
q0_event18.
.get_profiling_info<sycl::info::event_profiling::command_end>() 19.
q0_event20.
.get_profiling_info<sycl::info::event_profiling::command_start>();21.
22.
std::cout << "Execution time (iteration " << i23.
<< ") [sec]: " << (double)exec_time_ns0 * 1.0E-9 << "\n";24.
min_time_ns0 = std::min(min_time_ns0, exec_time_ns0);

パフォーマンス向上のため、特殊化定数を使用して変数 inner_loop_size を特殊化します。以下は、特殊化定数を使用して、デバイスで実行されるカーネルコードの一部です。
1. auto q0_event = q.submit([&](sycl::handler &h) {2.
// ランタイム変数を使用して特殊化定数を設定
3.
h.set_specialization_constant<trip_sc>(inner_loop_size);
4.
h.parallel_for<specialized_kernel>(5.
array_size / 2, [=](auto idx, sycl::kernel_handler kh) {6.
// トリップカウントを特殊化定数に設定
7.
auto runtime_trip_count_const =8.
kh.get_specialization_constant<trip_sc>();9.
auto accum = 0;
10.
for (size_t j = 0; j < runtime_trip_count_const; j++) {11.
auto multiplier = scalar * j;
12.
accum = accum + A0[idx] + B0[idx] * multiplier;
13.
}
14.
C0[idx] = accum;
15.
});
16.
});
17.
18.
q.wait();19.
20.
cl_ulong exec_time_ns0 =21.
q0_event22.
.get_profiling_info<sycl::info::event_profiling::command_end>() 23.
q0_event24.
.get_profiling_info<sycl::info::event_profiling::command_start>();25.
26.
std::cout << "Execution time (iteration " << i27.
<< ") [sec]: " << (double)exec_time_ns0 * 1.0E-9 << "\n";175

oneAPI GPU 最適化ガイド
最後に、特殊化されたトリップカウントの値と、通常の定数値を使用する例を比較します。以下は、通常の定数を使用
して、デバイスで実行されるカーネルコードの一部です。
1. auto q0_event = q.submit([&](sycl::handler &h) {2.
h.parallel_for<regular_constant_kernel>(array_size / 2, [=](auto idx) {3.
// トリップカウントを通常の定数に設定
4.
size_t runtime_trip_count_const = 10;
5.
auto accum = 0;
6.
for (size_t j = 0; j < runtime_trip_count_const; j++) {7.
auto multiplier = scalar * j;8.
accum = accum + A0[idx] + B0[idx] * multiplier;
9.
}10.
C0[idx] = accum;
11.
});
12.
});
13.
14.
q.wait();15.
16.
cl_ulong exec_time_ns0 =17.
q0_event18.
.get_profiling_info<sycl::info::event_profiling::command_end>() 19.
q0_event20.
.get_profiling_info<sycl::info::event_profiling::command_start>();21.
22.
std::cout << "Execution time (iteration " << i23.
<< ") [sec]: " << (double)exec_time_ns0 * 1.0E-9 << "\n";24.
min_time_ns0 = std::min(min_time_ns0, exec_time_ns0);

上記 3 つのバージョンの実行タイミングを以下に示します。ストリームサイズは、配列 A0、B0 および C0 のサイズを表わします。内部トリップカウントは、特殊化定数を使用して設定された runtime_trip_count_const 変数の値を表わします。
次の表は、入力として 134217728 要素 (1024MB) のストリームサイズと 10 回の内部トリップカウントを使用して実行したタイミング出力です。
•••

ランタイム変数で実行: 秒単位の時間 (最速): 0.00161008
特殊化定数で実行: 秒単位の時間 (最速): 0.00156256
定数で実行: 秒単位の時間 (最速): 0.00155104

結果は予想されたように、特殊化定数を使用すると、デバイス上の計算パフォーマンスは、ランタイム変数を使用した
実行時間から、定数を使用した実行時間に近くなったことが確認できます。さらに、生成されたコードを確認すると、特殊化されたアプリケーションでは、メインループがアンロールされていることが分かります。これは、ループのト
リップカウントを特殊化したことで、既知の定数として JIT コンパイルする機能が適用されているためです。特殊化定数を使用するプログラムのメインループは、ランタイム変数を使用する例と比較して、およそ inner_loop_size 倍(この例では 10 倍) の浮動小数点加算命令が生成されます。

5-14-4浮動小数点計算における精度とパフォーマンスのトレードオフ

浮動小数点を使用するアプリケーションの開発者は、通常、次の 2 つのことを意識します。
••

精度: 正確な計算結果に「近い」結果を生成します。
パフォーマンス: 可能な限り高速に実行できるアプリケーションを作成します。

通常、これら 2 つの目的は相反します。しかし、適切なプログラミング手法と適切なコンパイラー・オプションを使用することで、そのトレードオフを制御できます。

176

oneAPI GPU 最適化ガイド
詳細は、『インテル® コンパイラーの浮動小数点演算における結果の一貫性』 (Dr. Martyn J. Corden & David
Kreitzer 2018) を参照してください。
この節では、プログラマーがホストとデバイスで浮動小数点計算のセマンティクス (精度とパフォーマンス) を制御するメカニズム (コンパイラー・オプションとソースレベルの変更) について説明します。最初に OpenMP* と SYCL* プ
ログラムのコンパイラー・オプションについて、次に SYCL* のソースレベルの変更について説明します。

5-14-4-1

OpenMP*

OpenMP* では、-fp-model オプションを使用してホストとデバイスの浮動小数点計算のセマンティクスを制御できます。
•-fp-model=precise

このオプションは、浮動小数点計算を実装する際に、安全な値を保証する最適化を行うようコンパイラーに指示します。浮動小数点計算の結果を変える可能性のある最適化 (再関連付け、逆数による乗算、ゼロ・フォールディングなど）を無効にします。-fp-model=precise によって精度が向上すると、浮動小数点計算のパフォーマンスが低下する
可能性があります。
•-fp-model=fast

このオプションは、-O2 以上の最適化におけるホストとデバイス両方のコンパイルのデフォルトです。このオプション
は、浮動小数点計算を実装する際に、積極的な最適化を行うようコンパイラーに指示します。この最適化で速度は向上しますが、浮動小数点計算の精度と再現性に影響する可能性があります。
C/C++ では、-fp-model=fast オプションと -ffast-math オプションは同等です。このオプション (-O2
以 上 ) を 使 用 す る と 、 C/C++ フ ロ ン ト エ ン ド に よ っ て 7 つ の 高 速 演 算 フ ラ グ (nnan 、 ninf 、 nsz 、 arcp 、contract 、 afn 、 reassoc) が 設 定 さ れ ま す 。 (LLVM に お け る fast-math オ プ シ ョ ン に つ い て は 、
https://llvm.org/docs/LangRef.html#fast-math-flags (英語) を参照してください。)
一方、Fortran では、言語規則によってデフォルトで nnan フラグ (NaN なし) を設定できないことが規定されています。したがって、-fp-model=fast オプション (-O2 以上) では、6 つの高速演算フラグ (ninf、nsz、arcp、
contract、afn、reassoc) が設定されます。Fortran で 7 つのフラグを設定するには、-ffast-math オプション
を使用します。
-fp-model=fast (または -ffast-math) は、インテル® GPU (インテル® データセンター GPU マックス・シリーズ) ではネイティブの数学命令を有効にし ないことに注意し てください。ネイティブの数学命令では、-fp-model=fast よりも低い精度が得られます。インテル® GPU でネイティブの数学命令を有効にするには、
次のように -cl-fast-relaxed-math オプションを使用します。
•-Xopenmp-target-backend ”-options -cl-fast-relaxed-math”-fp-model=fast (または -ffast-math) は、インテル® GPU (インテル® データセンター GPU マックス・シリーズ) ではネイティブの数学命令を有効にしないことに注意してください。GPU でネイティブの数学命令を有効にするには、-Xopenmp-target-backend ”-options -cl-fast-relaxed-math” オプションでコンパイルする必要があります。ネイティブの数学命令では、-fp-model=fast よりも低い精度が得られます。

177

oneAPI GPU 最適化ガイド-Xopenmp-target-backend ”-options -cl-fast-relaxed-math” は、デバイス向けコンパイル・ツール・
チェーンのバックエンドに -cl-fast-relaxed-math オプションを渡します。-cl-fast-relaxed-math を指定すると、デバイスで一般的に使用される数学関数の精度が緩和されます。ネイティブ数学命令による精度要件が満たされている限り、多くの数学ライブラリー関数呼び出しでカーネルのパフォーマンスを向上する簡単な方法が提供されます。
-cl-fast-relaxed-math オプションは、プログラム全体のコンパイルに影響するため、結果の数値精度を細かく制御できなくなることに注意してください。
インテル® GPU は、単精度 (float、real) のネイティブ命令のみをサポートすることを忘れないでください。
注 (OpenMP*):•

コンパイルオプションに -fp-model が指定される場合 (-fopenmp-targets=spir64=”…” オプション
以外で)、-fp-model オプションはホストとデバイスの両方のコンパイルに適用されます。以下に示すように、ホストとデバイスのコンパイルに異なる -fp-model キーワードを指定できます。例えば、次のコンパイルコマ
ンドでは、ホストとデバイスの両方のコンパイルに -fp-model=precise を指定しています。
icx/icpx/ifx -O2 -fiopenmp -fopenmp-targets=spir64 -fp-model=precise

以下に示すように、ホストとデバイスのコンパイルに異なる -fp-model キーワードを指定できます。
•

ホストのコンパイルに -fp-model=fast を指定し、デバイスのコンパイルに -fp-model=precise を指定するには、次のようにします。
icx/icpx/ifx -O2 -fiopenmp -fopenmp-targets=spir64=”-fp-model=precise” -fp-model=fast

または
icx/icpx/ifx -O2 -fiopenmp -fopenmp-targets=spir64=”-fp-model=precise”(-O2 以上ではデフォルトであるため、-fp-model=fast は必要ありません。)•

また、ホストのコンパイルに -fp-model=precise を指定し、デバイスのコンパイルに -fp-model=fastを指定するには、次のようにします。
icx/icpx/ifx -O3 -fiopenmp -fopenmp-targets=spir64=”-fp-model=fast” -fp-model=precise•

ホストのコンパイルに -fp-model=fast を指定し、デバイスのコンパイルに relaxed-math を指定するには、次のようにします。
icx/icpx/ifx -O2 -fiopenmp -fopenmp-targets=spir64 -Xopenmp-target-backend “-options cl-fast-relaxed-math” -fp-model=fast

または
icx/ifx -O2 -fiopenmp -fopenmp-targets=spir64 -Xopenmp-target-backend “-options -clfast-relaxed-math”

178

oneAPI GPU 最適化ガイド(-O2 以上ではデフォルトであるため、-fp-model=fast は必要ありません。)•

ホストでの精度を調整するため、fast、precise、relaxed-math オプションと -fimf-precision=low(medium または high) を組み合わせることもできます。現在、デバイス向けに -fimf-precision オプショ
ンはサポートされていません。

以下の表では、OpenMP* の各種オプション (precise、fast-math、relaxed math) を (a) ホストとデバイスの両方 (2 列目) 、(b) ホストのみ (3 列目)、(c) デバイスのみ (4 列目) に設定する方法を示しています。
OpenMP* - オプションのまとめ
浮動小数点セマンティクス
precice

ホストとデバイスのコンパイルに適用-fp-model=precise

fast-math-fp-model=fast(デフォルト)relaxed-math(ネイティブ命令)

デバイスにのみ適用

5-14-4-2

ホストのコンパイルにのみ適用-fp-model=precise と-fiopenmp -fopenmptargets=spir64="-fpmodel=fast" を指定-fp-model=fast と-fiopenmp -fopenmptargets=spir64="-fpmodel= precise" を指定
デバイスにのみ適用

デバイスのコンパイルにのみ適用-fiopenmp -fopenmptargets=spir64="-fpmodel=precise"-fp-model=precise と-fiopenmp -fopenmptargets=spir64="-fpmodel=fast" を指定-Xopenmp-targetbackend "-options cl-fast-relaxedmath"

SYCL*

SYCL* で は 、 OpenMP* と 同 様 に 、 ホ ス ト と デ バ イ ス 両 方 の コ ン パ イ ル に -fp-model=fast オ プ シ ョ ン と-fp-model=precise オプションを使用できます。
SYCL* では、-fp-model=fast オプションと -ffast-math オプションは同等であり、-O2 以上ではホストとデバ
イスの両方のコンパイルでデフォルトです。
デバイスコンパイル向けに relaxed-math を有効にするには、-Xsycl-target-backend “-options -clfast-relaxed-math” オプションを使用します。GPU でネイティブ数学命令を使用するには、このオプションを指定する必要があります。
SYCL* (およびインテル® GPU) は、単精度 (float、real) のネイティブ命令のみをサポートします。
注 (SYCL*):•(-Xsycl-target オプション以外で) -fp-model コンパイルオプションを指定する場合、-fp-model オプ
ションはホストとデバイスの両方のコンパイルに適用されます。以下に示すように、ホストとデバイスのコンパイ
ルに異なる -fp-model キーワードを指定できます。例えば、次のコンパイルコマンドは、ホストとデバイスの両方のコンパイルに -fp-model=precise を指定しています。
icx/icpx/ifx -fsycl -fp-model=precise
以下に示すように、ホストとデバイスのこのパイルに異なる -fp-model キーワードを指定できます。

•

ホストのコンパイルに -fp-model=fast を指定し、デバイスのコンパイルに -fp-model=precise を指定するには、次のようにします。
icx/icpx -fsycl -Xsycl-target-frontend "-fp-model=precise" -fp-model=fast

179

oneAPI GPU 最適化ガイド
または
icx/icpx -fsycl -Xsycl-target-frontend "-fp-model=precise"(-O2 以上ではデフォルトであるため、-fp-model=fast は必要ありません。)•

また、ホストのコンパイルに -fp-model=precise を指定し、デバイスのコンパイルに -fp-model=fast を指定するには、次のようにします。
icx/icpx -fsycl -Xsycl-target-frontend "-fp-model=fast" -fp-model=precise•

ホストのコンパイルに -fp-model=fast を指定し、デバイスのコンパイルに relaxed-math を指定するには、次のようにします。
icx/icpx -fsycl -Xsycl-target-backend “-options -cl-fast-relaxed-math” -fp-model=fast

または
icx/icpx -fsycl -Xsycl-target-backend “-options -cl-fast-relaxed-math”(-O2 以上ではデフォルトであるため、-fp-model=fast は必要ありません。)
以下の表では SYCL* の各種オプション (precise、fast-math、relaxed math) を (a) ホストとデバイスの両方 (2列目) 、(b) ホストのみ (3 列目)、(c) デバイスのみ (4 列目) に設定する方法を示しています。
SYCL* - オプションのまとめ
浮動小数点のセマン
ティクス
precise

ホストとデバイスのコンパイ
ルに適用-fp-model=precise

fast-math-fp-model=fast(デフォルト)relaxed-math(ネイティブ命令)

デバイスにのみ適用

5-14-4-3

ホストのコンパイルにのみ適用-fp-model=precise と-Xsycl-target-frontend"-fp-mode=fast" を指定-fp-model=fast と-Xsycl-target-frontend"-fp-model=precise" を指定
デバイスにのみ適用

デバイスのコンパイルにのみ適用-Xsycl-targetfrontend "-fpmodel=precise"-fp-model=precise と-Xsycl-targetfrontend "-fpmodel=fast" を指定-Xsycl-target-backend"-options -cl-fastrelaxed-math"

ガイドライン

使用するオプションを決定するガイドラインを示します。
••••

矛盾するオプションを指定しない。結果は予測できません。
ホストとデバイスで最も一般的なオプションは、-fp-model=fast です。
デバイス上で最良のパフォーマンスを得るには relaxed-math を使用します。
最も高い精度が必要な場合 -fp-model=precise を使用します。

5-14-4-4

例: log 関数

数学ライブラリー (プログラミング言語の標準ライブラリーのコンポーネント) には、指数関数、対数関数、累乗関数、三角関数など、一般的な数学関数 (またはサブルーチン) が含まれます。
180

oneAPI GPU 最適化ガイド
しかし、数学ライブラリーの実装が異なると、精度が異なったり、同じ方法で丸めができない場合があります。数学ラ
イブラリー関数が返す値は、アルゴリズムと最適化が変更されると、コンパイラーのリリースごとに異なる場合があります。
数学ライブラリー関数の精度は、コンパイルオプションやソースコードで制御できます。log (自然対数) 数学関数の例を使用してこれを説明します。
OpenMP* / C++ (test_log_omp.cpp)
以下は、デバイス上で (OpenMP* ターゲット領域内で) std::log 関数を呼び出す OpenMP* プログラムです。このプログラムでは、一般的な数学関数の定義を含む cmath ヘッダーをインクルードします。
1. #include <iostream>
2. #include <assert.h>
3. #include <chrono>
4. #include <cmath>
5. #if FP_SIZE == 32
6.
typedef float FP_TYPE;
7.
static constexpr FP_TYPE VALIDATION_THRESHOLD = 1e-3;
8. #elif FP_SIZE == 64
9.
typedef double FP_TYPE;
10.
static constexpr FP_TYPE VALIDATION_THRESHOLD = 1e-6;
11. #endif
12. template<typename T>
13. void do_work (unsigned NELEMENTS, unsigned NREPETITIONS, T initial_value, T *res)14. {15. #pragma omp target teams distribute parallel formap(present,alloc:res[0:NELEMENTS])
16.
for (unsigned j = 0; j < NELEMENTS; j++)
17.
{
18.
T tmp = initial_value;19.
for (unsigned i = 0; i < NREPETITIONS; ++i)20.
tmp += std::log(tmp);21.
res[j] = tmp;
22.
}
23. }
24. int main (int argc, char *argv[])
25. {
26.
static constexpr unsigned NELEMENTS = 64*1024*1024;
27.
static constexpr unsigned NREPETITIONS = 1024;
28. #pragma omp target
29.
{ }
30.
FP_TYPE initial_value = 2;
31.
FP_TYPE ref_res = initial_value;32.
for (unsigned i = 0; i < NREPETITIONS; ++i)33.
ref_res += std::log(ref_res);34.
std::cout << "reference result = " << ref_res << std::endl;
35.
{
36.
FP_TYPE * std_res = new FP_TYPE[NELEMENTS];
37.
assert (std_res != nullptr);
38.
std::chrono::duration<float, std::micro> elapsed;
39.
#pragma omp target data map(std_res[0:NELEMENTS])
40.
{
41.
auto tbegin = std::chrono::system_clock::now();42.
do_work<FP_TYPE> (NELEMENTS, NREPETITIONS, initial_value, std_res);43.
auto tend = std::chrono::system_clock::now();
44.
elapsed = tend - tbegin;
45.
}
46.
std::cout << "std::log result[0] = " << std_res[0] << std::endl;
47.
bool allequal = true;48.
for (auto i = 1; i < NELEMENTS; ++i)49.
allequal = allequal and std_res[0] == std_res[i];50.
if (allequal)
51.
{

181

oneAPI GPU 最適化ガイド
52.
53.
54.

if (std::abs(ref_res - std_res[0])/std::abs(ref_res) <std::abs(VALIDATION_THRESHOLD))
std::cout << "std::log validates. Total execution time is"<<elapsed.count()
55.
<< " us." << std::endl;
56.
else
57.
std::cout << "std::log does not validate (ref=" << ref_res
58.
<< " std_res=" << std_res[0]59.
<< " mix=" << std::abs(ref_res std_res[0])/std::abs(ref_res)<<")"
60.
<< std::endl;
61.
}
62.
else
63.
std::cout << "std::log does not validate, results are not equal." <<std::endl;
64.
delete [] std_res;
65.
}
66.
return 0;
67. }test_log_omp.cpp サンプルのコンパイルと実行コマンド:$ icpx -O2 -fiopenmp -fopenmp-targets=spir64 test_log_omp.cpp -DREAL_ELEMENT-DFP_SIZE=64 -fp-model=fast -fopenmp-version=51$ export ZE_AFFINITY_MASK=0.0$ ./a.out

OpenMP* / Fortran (test_log_omp_f_mod.f90)
以下は、デバイス上で (OpenMP* ターゲット領域から) Fortran 組込み数学 log 関数を呼び出す OpenMP* Fortran
モジュールです。
1.
2.
3.
4.
5.
6.
7.

MODULE test
USE ISO_C_BINDING
CONTAINSSUBROUTINE log_real_sp (nelements, nrepetitions, initial_value, res)bind(C,NAME='log_real_sp')
8.
IMPLICIT NONE
9.
INTEGER(KIND=C_INT), VALUE :: nelements, nrepetitions
10.
REAL(C_FLOAT), VALUE :: initial_value
11.
REAL(C_FLOAT) :: res(0:nelements-1), tmp
12.
INTEGER :: i, j
13.
14.
!$OMP TARGET TEAMS DISTRIBUTE PARALLEL DO PRIVATE(tmp)15.
DO j = 0, nelements-1
16.
tmp = initial_value
17.
DO i = 0, nrepetitions-1
18.
tmp = tmp + log(tmp)
19.
END DO20.
res(j) = tmp
21.
END DO
22.
RETURN23.
END SUBROUTINE log_real_sp
24.
25.
SUBROUTINE log_real_dp (nelements, nrepetitions, initial_value, res)bind(C,NAME='log_real_dp')
26.
IMPLICIT NONE
27.
INTEGER(KIND=C_INT), VALUE :: nelements, nrepetitions
28.
REAL(C_DOUBLE), VALUE :: initial_value
29.
REAL(C_DOUBLE) :: res(0:nelements-1), tmp
30.
INTEGER :: i, j
31.
32.
!$OMP TARGET TEAMS DISTRIBUTE PARALLEL DO PRIVATE(tmp)

182

oneAPI GPU 最適化ガイド
33.
DO j = 0, nelements-1
34.
tmp = initial_value
35.
DO i = 0, nrepetitions-1
36.
tmp = tmp + log(tmp)37.
END DO38.
res(j) = tmp
39.
END DO
40.
RETURN41.
END SUBROUTINE log_real_dp
42.
43. END MODULE test

次のコマンドで test_log_omp_f.f90 をコンパイルします。
$ ifx -c -O2 -fiopenmp -fopenmp-targets=spir64 test_log_omp_f_mod.f90

SYCL* (test_log_sycl.cpp)
SYCL* では、呼び出す数学関数を選択することで、ソースレベルで浮動小数点セマンティクスを制御できます。例えば、
SYCL* プログラムで次の 3 つの異なる log 関数を呼び出す例を考えてみます。
•

std::log: C++ 標準ライブラリーの log 関数を参照します。実装の選択は、コンパイルオプション (-fpmodel と -cl-fast-relaxed-math) の規定によって決定されます。例えば、ネイティブ数学命令を使用する実装を選択するには、-cl-fast-relaxed-math オプションを指定してコンパイルします。

•

sycl::log: SYCL* が提供する sycl 名前空間の log 関数を参照します。-cl-fast-relaxed-math オ
プションが指定されていなくても、この関数はネイティブ命令を使用する場合があります。精度については
SYCL* 仕様で詳しく説明されています。

•

sycl::native::log: SYCL* が提供する sycl 名前空間のネイティブ log 関数を参照します。この関数はネイティブ命令を使用するため、-cl-fast-relaxed-math オプションは必要ありません。SYCL* (およびインテル® GPU) は、単精度 (float、real) のネイティブ命令のみをサポートします。精度については SYCL*
仕様で詳しく説明されています。
1. #include <CL/sycl.hpp>
2. #include <iostream>
3. #include <assert.h>
4. #include <chrono>
5. #include <cmath>
6. #if FP_SIZE == 32
7.
typedef float FP_TYPE;
8.
static constexpr FP_TYPE VALIDATION_THRESHOLD = 1e-3;
9. #elif FP_SIZE == 64
10.
typedef double FP_TYPE;
11.
static constexpr FP_TYPE VALIDATION_THRESHOLD = 1e-6;
12. #endif
13. template<typename T>
14. void do_work_std (sycl::queue &q, unsigned NELEMENTS, unsigned NREPETITIONS, T
initial_value, T *res)
15. {
16.
q.submit([&](sycl::handler& h) {17.
h.parallel_for(NELEMENTS, [=] (auto j)
18.
{
19.
FP_TYPE tmp = initial_value;20.
for (unsigned i = 0; i < NREPETITIONS; ++i)21.
tmp += std::log(tmp);22.
res[j] = tmp;
23.
});
24.
}).wait();
25. }26. template<typename T>
27. void do_work_sycl (sycl::queue &q, unsigned NELEMENTS, unsigned NREPETITIONS, T
initial_value, T *res)183

oneAPI GPU 最適化ガイド
28. {
29.
q.submit([&](sycl::handler& h) {30.
h.parallel_for(NELEMENTS, [=] (auto j)
31.
{
32.
FP_TYPE tmp = initial_value;33.
for (unsigned i = 0; i < NREPETITIONS; ++i)34.
tmp += sycl::log(tmp);35.
res[j] = tmp;
36.
});
37.
}).wait();
38. }39. # if FP_SIZE == 32
40. template<typename T>
41. void do_work_sycl_native (sycl::queue &q, unsigned NELEMENTS, unsigned
NREPETITIONS, T initial_value, T *res)
42. {
43.
q.submit([&](sycl::handler& h) {44.
h.parallel_for(NELEMENTS, [=] (auto j)
45.
{
46.
FP_TYPE tmp = initial_value;47.
for (unsigned i = 0; i < NREPETITIONS; ++i)48.
tmp += sycl::native::log(tmp);49.
res[j] = tmp;
50.
});
51.
}).wait();
52. }53. # endif
54. int main (int argc, char *argv[])
55. {
56.
static constexpr unsigned NELEMENTS = 64*1024*1024;
57.
static constexpr unsigned NREPETITIONS = 1024;58.
sycl::device d (sycl::gpu_selector_v);
59.
sycl::queue q (d);60.
q.submit([&](sycl::handler& h) {61.
h.single_task ([=]() { });62.
}).wait();63.
FP_TYPE initial_value = 2;64.
FP_TYPE ref_res = initial_value;65.
for (unsigned i = 0; i < NREPETITIONS; ++i)66.
ref_res += std::log(ref_res);67.
std::cout << "reference result = " << ref_res << std::endl;
68.
{
69.
FP_TYPE * std_res = new FP_TYPE[NELEMENTS];
70.
assert (std_res != nullptr);
71.
std::chrono::duration<float, std::micro> elapsed;
72.
{
73.
auto * res = sycl::malloc_device<FP_TYPE>(NELEMENTS, q);74.
auto tbegin = std::chrono::system_clock::now();75.
do_work_std<FP_TYPE>(q, NELEMENTS, NREPETITIONS, initial_value, res);76.
auto tend = std::chrono::system_clock::now();77.
elapsed = tend - tbegin;78.
q.memcpy (std_res, res, NELEMENTS*sizeof(FP_TYPE)).wait();79.
sycl::free (res, q);
80.
}
81.
std::cout << "std::log result[0] = " << std_res[0] << std::endl;
82.
bool allequal = true;83.
for (auto i = 1; i < NELEMENTS; ++i)84.
allequal = allequal and std_res[0] == std_res[i];85.
if (allequal)
86.
{
87.
if (std::abs(ref_res - std_res[0])/std::abs(ref_res) <std::abs(VALIDATION_THRESHOLD))
88.
std::cout << "std::log validates.Total execution time is "89.
<< elapsed.count() << " us."<< std::endl;
90.
else
91.
std::cout << "std::log does not validate (ref=" << ref_res << " std_res="92.
<< std_res[0] << " mix="
93.
<< std::abs(ref_res - std_res[0])/std::abs(ref_res) << ")"94.
<< std::endl;

184

oneAPI GPU 最適化ガイド
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.

}
else
std::cout << "std::log does not validate, results are not equal."<< std::endl;
delete [] std_res;}{
FP_TYPE * sycl_res = new FP_TYPE[NELEMENTS];
assert (sycl_res != nullptr);

std::chrono::duration<float, std::micro> elapsed;{
auto * res = sycl::malloc_device<FP_TYPE>(NELEMENTS, q);auto tbegin = std::chrono::system_clock::now();
do_work_sycl<FP_TYPE>(q, NELEMENTS, NREPETITIONS, initial_value, res);auto tend = std::chrono::system_clock::now();
elapsed = tend - tbegin;q.memcpy (sycl_res, res, NELEMENTS*sizeof(FP_TYPE)).wait();
sycl::free (res, q);}
std::cout << "sycl::log result[0] = " << sycl_res[0] << std::endl;
bool allequal = true;for (auto i = 1; i < NELEMENTS; ++i)
allequal = allequal and sycl_res[0] == sycl_res[i];if (allequal){
if (std::abs(ref_res - sycl_res[0])/std::abs(ref_res) <std::abs(VALIDATION_THRESHOLD))
121.
std::cout << "sycl::log validates. Total execution time is "122.
<< elapsed.count() << " us." << std::endl;
123.
else
124.
std::cout << "sycl::log does not validate (ref=" << ref_res
125.
<< " sycl_res=" << sycl_res[0]126.
<< " mix=" << std::abs(ref_res - sycl_res[0])/std::abs(ref_res)
127.
<< ")" << std::endl;
128. }
129. else
130.
std::cout << "sycl::log does not validate, results are not equal." <<std::endl;
131. delete [] sycl_res;132. }
133. # if FP_SIZE == 32
134. {
135. FP_TYPE * sycl_native_res = new FP_TYPE[NELEMENTS];
136. assert (sycl_native_res != nullptr);
137. std::chrono::duration<float, std::micro> elapsed;
138. {
139. auto * res = sycl::malloc_device<FP_TYPE>(NELEMENTS, q);140. auto tbegin = std::chrono::system_clock::now();
141. do_work_sycl_native<FP_TYPE>(q, NELEMENTS, NREPETITIONS, initial_value, res);142. auto tend = std::chrono::system_clock::now();
143. elapsed = tend - tbegin;144. q.memcpy (sycl_native_res, res, NELEMENTS*sizeof(FP_TYPE)).wait();145. sycl::free (res, q);146. }
147. std::cout << "sycl::native::log result[0] = " << sycl_native_res[0] << std::endl;
148. bool allequal = true;
149. for (auto i = 1; i < NELEMENTS; ++i)
150. allequal = allequal and sycl_native_res[0] == sycl_native_res[i];151. if (allequal)152. {
153.
if (std::abs(ref_res - sycl_native_res[0])/std::abs(ref_res) <std::abs(VALIDATION_THRESHOLD))
154.
std::cout << "sycl::native::log validates. Total execution time is "155.
<< elapsed.count() << " us."<< std::endl;
156.
else
157.
std::cout << "sycl::native::log does not validate (ref=" << ref_res
158.
<< " sycl_native_res=" << sycl_native_res[0]159.
<< " mix=" << std::abs(ref_res sycl_native_res[0])/std::abs(ref_res)185

oneAPI GPU 最適化ガイド
160.
<< ")" << std::endl;
161. }
162. else163.
std::cout << "sycl::native::log does not validate, results are not equal."164.
<< std::endl;
165. delete [] sycl_native_res;166. }
167. # endif // FP_SIZE == 32
168. return 0;169. }

test_log_sycl.cpp サンプルのコンパイルと実行コマンド:$ icpx -fsycl -O2 test_log_sycl.cpp -DREAL_ELEMENT -DFP_SIZE=64 -fp-model=fast$ export ZE_AFFINITY_MASK=0.0$ ./a.out

5-14-4-5

パフォーマンス検証

以下に、log 関数を呼び出す異なるプログラム (OpenMP* C++、OpenMP* Fortran および SYCL*) を実行した結果をまとめます。ここでは、インテル® データセンター GPU マックス・シリーズ (1 スタックのみ) を使用し、単精度の log関数のパフォーマンスを取得しています。
log のパフォーマンス - デフォルト精度 (Fast-math)バ―ション
OpenMP*/C++ (std::log)
OpenMP*/Fortran (log)SYCL* (std::log)
SYCL* (sycl::log)
SYCL* (sycl::native::log)

時間 (秒)
93,11894,342
31,83531,644
31,684log のパフォーマンス - Fast-math
バ―ション
OpenMP*/C++ (std::log)
OpenMP*/Fortran (log)SYCL* (std::log)
SYCL* (sycl::log)
SYCL* (sycl::native::log)

時間 (秒)
93,181
94,46731,65732,064
31,452log のパフォーマンス – Precise
バ―ション
OpenMP*/C++ (std::log)
OpenMP*/Fortran (log)SYCL* (std::log)
SYCL* (sycl::log)
SYCL* (sycl::native::log)

時間 (秒)
92,971
94,444
94,592
94,852
40,778log のパフォーマンス - Relaxed-math
バ―ション
OpenMP*/C++ (std::log)
OpenMP*/Fortran (log)SYCL* (std::log)
SYCL* (sycl::log)
SYCL* (sycl::native::log)

186

時間 (秒)
35,25135,78731,31432,07732,141

oneAPI GPU 最適化ガイド
所見:•

OpenMP* (C および Fortran) では、std::log はコンパイルオプション (-fp-model、-cl-relaxed-math)の規定に従います。-cl-relaxed-math オプションを指定すると、ネイティブ命令の std::log を呼び出します。

•

SYCL* では、sycl::log は -fp-model=fast だけでもネイティブ命令を使用する場合があります。

•

SYCL* では、sycl::native::log は常にネイティブマシン命令を使用します。-cl-relaxed-math オプ
ションは必要ありません。

•

OpenMP* と SYCL* では、-cl-relaxed-math オプションを指定すると、デバイスの log 関数にネイティブマ
シン命令が使用されます。

•-fp-model=precise は、正確な結果を生成しますが、パフォーマンスは低下します。

5-14-4-6

参考資料

1.

「インテル® コンパイラーの浮動小数点演算における結果の一貫性」 (Dr. Martyn J. Corden and David
Kreitzer (2018))

2.

『LLVM 言語リファレンス・マニュアル』 - 「Fast-Math フラグ」 (英語)3.

『DPC++ コンパイラー・ドキュメント』 - 「ユーザーズ・マニュアル」 (英語)4.

『インテル® oneAPI DPC++/C++ コンパイラー・デベロッパー・ガイドおよびリファレンス』 - 「サポートされるオ
プション」 (英語)5.

『インテル® oneAPI DPC++/C++ コンパイラー・デベロッパー・ガイドおよびリファレンス』 - 「Xopenmptarget」 (英語)6.

『インテル® Core™ プロセッサーおよびインテル® Xeon® プロセッサー向け OpenCL* デベロッパー・ガイド』
(英語)

7.

『インテル® SDK for OpenCL* アプリケーション・デベロッパー・ガイド』 (英語)8.

ターゲット・ツールチェーン・オプション (英語)9.

『SYCL 2020 仕様』 - 「数学関数」

10. 『SYCL 2020 仕様』 - 「SYCL* ホストとデバイス向けの SYCL* ビルトイン関数」
11. 『OpenCL* 仕様バージョン 1.2』 (英語) Khronos OpenCL* ワーキンググループ
12. 『インテル® プロセッサー・グラフィックス向け OpenCL* デベロッパー・ガイド』 (英語)187

oneAPI GPU 最適化ガイド

5-15 OpenMP* オフロードのチューニング・ガイド
LLVM ベースのインテル® C/C++ および Fortran コンパイラー (icx、icpx、および ifx) は、GPU への OpenMP*
オフロードをサポートします。OpenMP* を使用する場合、プログラマーはソースコードにデバイス・ディレクティブを挿入して、アプリケーションの特定の領域を GPU にオフロードするようにコンパイラーに指示します。計算集約型のコードをオフロードすることで、適切なパフォーマンスを期待できます。
この節では、OpenMP* オフロードに関連する各種トピックと、オフロードされたコードのパフォーマンスを向上させる方法について説明します。
•
•
•
•
•
•
•

OpenMP* ディレクティブ
OpenMP* 実行モデル
用語
OpenMP* アプリケーションのコンパイルと実行
oneMKL の計算を GPU へオフロードする
OpenMP* アプリケーションのパフォーマンスを解析するツール
OpenMP* オフロードの最良の事例

5-15-1OpenMP* ディレクティブ

インテル® コンパイラー (icx、icpx および ifx) は、計算のオフロードとデバイスへのデータマッピングを制御する
さまざまな OpenMP* デバイス・ディレクティブをサポートしています。これには以下のものがあります。
•
•
•
•
•
•
•
•
•

targetteams
distributetarget datatarget enter datatarget exit datatarget update
declare target
dispatchtarget 構造は、デバイスで実行するコード領域と、データをデバイスにマッピングする方法を指定します。
teams 構造は、プライマリー・スレッドと複数のワーカースレッドで構成されるスレッドのチームを作成します。
teams が num_teams 節なしで指定されると、チーム数は実装定義になります。
distribute 構造は、ループ反復を各チームのプライマリー・スレッドに分散させます。
target data 構造は、デバイスのデータ環境に変数をマップします。変数は、任意の map 節によって target data
領域の範囲にマッピングされます。
target enter data ディレクティブでは、デバイスへマップする変数を指定します。このディレクティブの map 節で指定されるマップタイプは to または alloc である必要があります。
target exit data ディレクティブは、変数がデバイスからマップ解除されることを指示します。このディレクティブの map 節で指定できるマップタイプは、from、release または delete です。

188

oneAPI GPU 最適化ガイド
target update ディレクティブは、指定される motion 節にしたがって、デバイス上の変数値と元のホストの変数と一致させます。
declare target ディレクティブは、デバイスにマップされる変数、関数 (C/C++、Fortran) およびサブルーチン(Fortran) を指定します。
declare variant 指示句は、ベース関数の特殊バリアントを宣言し、そのバリアントが使用されるコンテキストを指定します。
dispatch 構造は、関数呼び出しに対しバリアントへの置き換えを行うかどうか制御します。
map 節は、元のホスト変数をデバイス上の対応する変数にどのようにマップするかを決定します。マップタイプには以下があります。
••••

to: ホストのオリジナル変数が target 領域の入り口でデバイスにコピーされます。
from: デバイス上の変数の値は、target 領域の出口でデバイスから元のホスト変数にコピーされます。
tofrom: 元のホスト変数の値は、target 領域の入り口でデバイスにコピーされ、target 領域の出口でホストにコピーバックされます。
alloc: 元のホストの変数と同じタイプとサイズの変数をデバイスに割り当てます (値はホストからコピーされません)。

ディレクティブは組み合わせることができます。例えば、次のように組み合わせることができます。
••••

target teams
target teams distributetarget teams distribute parallel fortarget teams distribute parallel for simd

コンパイラーとランタイムがオフロードされたループ反復を GPU で実行するため最良の方法で分割できるよう、可能な限り複合ディレクティブを使用することを推奨します。

5-15-2OpenMP* 実行モデル

OpenMP* 実行モデルでは、ホストデバイスは単一ですが、複数のターゲットデバイスを持つことができます。デバイ
スは論理実行エンジンであり、固有のローカルストレージとデータ環境を持ちます。
インテル® データセンター GPU マックス・シリーズで実行する場合、2 つのタイルで構成される 1 つの GPU デバイス、または各タイルをそれぞれデバイスと見なすことができます。
OpenMP* ではホスト上で実行を開始します。ホストスレッドが target 構造に到達すると、データはホストからデバ
イスに転送され (例えば、map 節で指定されている場合)、構造内のコードはデバイスにオフロードされます。target
領域の終端では、データはデバイスからホストに転送されます (指定されていれば)。
デフォルトでは、ホストスレッドは target 領域が完了するのを待って処理を続行します。target 構造に nowait
節が指定されると、ホストスレッドは target 領域の完了を待機せずに処理を続行します。つまり、nowait 節を使用
すると target 領域を非同期に実行できます。
非同期に実行されるコード領域間の同期は、taskwait ディレクティブ、depend 節、(暗黙および明示的な) バリア、または他の同期メカニズムによって行うことができます。

189

oneAPI GPU 最適化ガイド

5-15-3

用語

ここでは、オフロードされた並列ループの反復処理を説明するため、OpenMP* と SYCL* の用語を同じ意味で使用しています。
「スレッドのマッピングと GPU 占有率」で説明したように、GPU にオフロードされた並列ループ (実行レンジ) の反復
は、work-group、sub-group、そして work-item に分割されます。ND-range はすべての実行範囲を表しており、同
じサイズの work-group に分割されます。work-group は、1 次元、２ 次元、または 3 次元の work-item の集合です。
各 work-group は sub-group に分割できます。sub-group は、SIMD ベクトルとして同時に処理される連続した
work-item の短い範囲を表します。
次の表は、SYCL* の概念が OpenMP* と CUDA* の概念とどのように関連するか示しています。
SYCL*
ワーク項目
ワークグループ
ワークグループ・サイズ
ワークグループ数
サブグループワークグループあたりのワーク項目の最大数

5-15-4OpenMP*
OpenMP* スレッドまたは SIMD レーン
チーム
チームサイズ
チーム数
SIMD チャンク (simdlen = 8、16、32)スレッドリミット

CUDA*
CUDA* スレッド
スレッドブロック
スレッド・ブロック・サイズ
スレッドブロック数
ワープ (size = 32)
スレッドブロックあたりの CUDA* ス
レッドの最大数

OpenMP* アプリケーションのコンパイルと実行

インテル® GPU への OpenMP*オフロードを有効にするには、次のコンパイルオプションを使用します。C/C++ とFortran の両方に適用できます。
-fiopenmp -fopenmp-targets=spir64

デフォルトでは、インテル® コンパイラーはプログラムを SPIR-V* と呼ばれる中間言語に変換し、コンパイルプロセ
スで生成されたバイナリーに格納します。実行時に SPIR-V* コードをプラットフォームのアセンブリー・コードに変換することで、いずれのハードウェア・プラットフォームでもコードを実行できます。このプロセスを JIT (Just-In-Time)
コンパイルと呼びます。
次のコンパイルオプションを追加して、コンパイラー最適化レポートの出力を有効にします。
-qopt-report=3 -O3

注:-qopenmp は、-fiopenmp と等価であり、この 2 つのオプションには互換性があります。

190

oneAPI GPU 最適化ガイド

5-15-4-1

事前コンパイル (AOT)インテル® データセンター GPU マックス・シリーズの事前コンパイル (AOT) コンパイルには、次のようなコンパイル
オプション (-Xs) を使用します。このオプションは、C/C++ と Fortran 両方で使用できます。
-fiopenmp -fopenmp-targets=spir64_gen -Xs "-device pvc"

5-15-4-2

OpenMP* ランタイムルーチン

デバイスに関連するランタイムルーチンには次のものがあります。
omp_target_alloc
omp_target_free
omp_target_memcpy

次のランタイムルーチンは、インテル® コンパイラーの拡張としてサポートされます。
omp_target_alloc_host
omp_target_alloc_device
omp_target_alloc_shared

omp_target_free を呼び出すと、インテル拡張のルーチンで割り当てたメモリーも解放できます。
icx、icpx および ifx コンパイラーでサポートされる OpenMP* 機能は以下を参照してください。
••

インテル® oneAPI DPC++/C++ コンパイラーにおける OpenMP* 機能と拡張のサポート
インテル® Fortran コンパイラーで実装される Fortran 言語と OpenMP* 機能

5-15-4-3

環境変数

ここでは、プログラムのデバッグやパフォーマンス向上に役立つ環境変数を紹介します。
環境変数に関する追加情報は以下を参照してください。
••••『インテル® oneAPI DPC++/C++ コンパイラー・デベロッパー・ガイドおよびリファレンス』 - 「サポートされる環
境変数」 (英語)『インテル® oneAPI プログラミング・ガイド』 - 「デバッグツール」
LLVM* OpenMP* ランタイム (英語)
レベルゼロプラグイン向けのデバッグ変数 (英語)

LIBOMPTARGET_DEBUG=1
ライブラリーからのデバッグ情報出力を有効にします。
LIBOMPTARGET_DEVICES=<DeviceKind>
サブデバイスをユーザーにどのように公開するか制御します。
<DeviceKind> := DEVICE | SUBDEVICE | SUBSUBDEVICE | device | subdevice | subsubdevice

191

oneAPI GPU 最適化ガイド
DEVICE/device: OpenMP* デバイスとしてレポートされるのはトップレベルのデバイスのみです。subdevice 節
はオプションとしてサポートされます。
SUBDEVICE/subdevice: 第 1 レベルのサブデバイスのみが OpenMP* デバイスとしてサポートされ、subdevice
節は無視されます。
SUBSUBDEVICE/subsubdevice: 第 2 レベルのサブデバイスのみが OpenMP* デバイスとしてサポー
トされ、subdevice 節は無視されます。レベルゼロ・バックエンドを使用する インテル® GPU で、サブのサ
ブ デ バ イ ス を タ イ ル 内 の 単 一 計 算 ス ラ イ ス と し て 制 限 す る に は 、 追 加 の GPU 計 算 ラ ン タ イ ム 環 境 変 数
CFESingleSliceDispatchCCSMode=1 も設定する必要があります。
デフォルトは、<DeviceKind>=device です。
LIBOMPTARGET_INFO=<Num>
ユーザーが libomptarget から各種タイプのランタイム情報を要求できるようにします。詳細については、
https://openmp.llvm.org/design/Runtimes.html#libomptarget-info (英語) を参照してください。
LIBOMPTARGET_LEVEL0_MEMORY_POOL=<Option>
再利用可能なメモリープールの構成を制御します。
<Option>:= 0 | <PoolInfoList><PoolInfoList> := <PoolInfo>[,<PoolInfoList>]<PoolInfo>:= <MemType>[,<AllocMax>[,<Capacity>[,<PoolSize>]]]<MemType>:= all | device | host | shared<AllocMax>:= 正の整数または空 (MB 単位の最大割り当てサイズ)<Capacity>:= 正の整数または空 (単一ブロックからの割り当て数)<PoolSize>:= 正の整数または空 (MB 単位の最大プールサイズ)

プールは、単一ブロックから <AllocMax> サイズまで、少なくとも <Capacity> 個の割り当てに対応できるメモ
リーブロックのリストであり、合計サイズが <PoolSize> を超えないようにします。
LIBOMPTARGET_LEVEL0_STAGING_BUFFER_SIZE=<Num>
ステージのバッファーサイズを <Num> に KB 単位で設定します。ステージバッファーは、ホストメモリーが USM ではない場合に、ホストとデバイス間のコピー操作を最適化するために使用されます。ステージバッファーは、ディスクリー
ト・デバイスのみに使用され、デフォルトのバッファーサイズは 16KB です。
LIBOMPTARGET_LEVEL_ZERO_COMMAND_BATCH=copy
target 領域のデータ転送コマンドのバッチ処理を可能にします。
target 構造に map(to:) 節が指定されている場合、この環境変数はホストからデバイスへ複数のデータ転送を同時に行うことを許可します。同様に、target 構造に map(from:) 節がある場合、この環境変数はデバイスからホストへ複数のデータ転送を同時に行うことを許可します。map(tofrom:) や map() は map(to:) と map(from:)に分割されることに注意してください。

192

oneAPI GPU 最適化ガイド
LIBOMPTARGET_LEVEL_ZERO_USE_IMMEDIATE_COMMAND_LIST=<Bool>
カーネル送信に即時コマンドリストの使用を有効/無効にします。
<Bool> := 1 | T | t | 0 | F | f

デフォルトでは、即値コマンドリストは無効 (0) になっています。
LIBOMPTARGET_PLUGIN=<Name>
使用するオフロードプラグインの名称を指定します。
<Name> := LEVEL0 | OPENCL | X86_64 |level0 | opencl | x86_64

デフォルトでは、オフロードプラグインは LEVEL0 です。
LIBOMPTARGET_PLUGIN_PROFILE=<Enable>[,<Unit>]
基本プラグインのプロファイルを有効にし、プログラムの終了時に結果を表示します。
<Enable> := 1 | T<Unit> := usec | unit_usec

デフォルトではプラグインのプロファイルは無効になっています。
<Unit> が指定されていない場合、マイクロ秒 (usec) がデフォルト単位になります。
LIBOMPTARGET_PROFILE=<FileName>libomptarget.so が Clang の -ftime-trace オプションと同様の時間プロファイルの出力を生成できるようにします。
OMP_TARGET_OFFLOAD=MANDATORY
デバイス構造またはデバイス・メモリー・ルーチンに到達した際に、デバイスが利用できない、または実装でサポートされていない場合、プログラムの実行を終了することを指定します。
暗黙的または明示的にスケーリングを制御する環境変数
暗黙のスケーリングを無効にして、単一の GPU タイルのみを利用するには、ZE_AFFINITY_MASK=0.0 を設定します。
明示的なスケーリングを有効にするには、LIBOMPTARGET_DEVICES=subdevice を設定します。
インテル® データセンター GPU マックス・シリーズの場合、デフォルトで暗黙のスケーリングは有効になっています。
SYCL* 向けの環境変数
OpenMP* だけではなく、SYCL* プログラムの開発とデバッグに有効な環境変数 (SYCL_PI_LEVEL_ZERO) があります。詳細については、「インテル® oneAPI ツールキット 2023 における DPC++ ランタイム環境変数」をご覧ください。
193

oneAPI GPU 最適化ガイド

5-15-4-4

参考資料

1.

インテル® oneAPI DPC++/C++ コンパイラーにおける OpenMP* 機能と拡張のサポート

2.

インテル® Fortran コンパイラーで実装される Fortran 言語と OpenMP* 機能

3.

『インテル® oneAPI DPC++/C++ コンパイラー・デベロッパー・ガイドおよびリファレンス』 - 「サポートされる
環境変数」 (英語)4.

『インテル® oneAPI プログラミング・ガイド』 - 「デバッグツール」

5.

LLVM* OpenMP* ランタイム (英語)6.

レベルゼロプラグイン向けのデバッグ変数 (英語)7.

インテル® oneAPI ツールキット 2023 における DPC++ ランタイム環境変数

5-15-5

oneMKL の計算を GPU へオフロードする

oneAPI マス・カーネル・ライブラリー (oneMKL) は、大規模な計算問題を解決するソフトウェア・アプリケーションにおける数学ルーチンのパフォーマンスを向上させます。oneMKL は BLAS および LAPACK 線形代数ルーチン、高速フーリエ変換、ベクトル数学関数、乱数生成関数、およびその他の機能を提供します。
oneMKL の配布物には、oneMKL ルーチンのさまざまな呼び出し例を含む examples ディレクトリーが含まれています。
インテル® oneAPI マス・カーネル・ライブラリーの詳細については、以下を参照してください。
•••

インテル® oneAPI マス・カーネル・ライブラリー・デベロッパー・リファレンス - C (英語)インテル® oneAPI マス・カーネル・ライブラリー・デベロッパー・リファレンス - Fortran (英語)
バッチ GEMM 操作の紹介 (英語)

5-15-5-1

oneMKL OpenMP* オフロードを使用する際のコンパイルコマンド

この項で説明する内容は、Linux* 固有のものです。
OpnMP スレッドを使用し、oneMKL API を呼び出す C/C++ または Fortran プログラムのコンパイルコマンドは次のとおりです。
C/C++: icx/icpx -fiopenmp -fopenmp-targets=spir64 -qmkl source.cpp
Fortran: ifx -fiopenmp -fopenmp-targets=spir64 -qmkl source.cpp

Fortran 使用時の注意
oneMKL LP64 ライブラリーは 32 ビット整数タイプ、oneMKL ILP64 ライブラリーは 64 ビット整数タイプ (231-1
要素を持つ大規模配列のインデックスに必要) で配列のインデックスを作成します。
Fortran プログラムで、32 ビット整数タイプの配列をインデックス付けする場合は、プログラムに次の use ステート
メントを含めます。
use onemkl_blas_omp_offload_lp64

194

oneAPI GPU 最適化ガイド
一方、64 ビット整数タイプで配列をインデックスする場合は、プログラムに次の use ステートメントを含めます。
use onemkl_blas_omp_offload_ilp64-qmkl オプション使用時の注意-qmkl オプション (-qmkl=parallel と同等) を使用して、指定するスレッド化オプションに応じて、特定の
oneMKL スレッド化レイヤーとリンクします。
••-fiopenmp: インテル® コンパイラーの OpenMP* スレッド化レイヤー-tbb: インテル® スレッディング・ビルディング・ブロック (インテル®TBB) のスレッド化レイヤー-qmkl=sequential を使用して、oneMKL ランタイム変数のシリアルバージョンとリンクします。
-qmkl=parallel/sequential は、CPU のスレッド化のみに影響することに注意してください。オフロードされた
oneMKL 計算は常に適切に並列化され、可能な限り多くの GPU ベクトルエンジンを占有します。

5-15-5-2

oneMKL 計算をオフロードする OpenMP* ディレクティブ

OpenMP* ディレクティブを使用して、oneMKL 計算を GPU にオフロードできます。
dispatch ディレクティブ
oneMKL の計算を GPU にオフロードする際に推奨される方法は、OpenMP* 5.1 dispatch ディレクティブを使用することです。次の例に示すように、ディスパッチ構造内に oneMKL ルーチンの呼び出しを配置します。
#pragma omp target data map(to: A[0:m*k], B[0:k*n]) map(tofrom: C[0:m*n]){#pragma omp dispatch
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A, k, B, n, beta, C, n);}

上記の例では、dispatch 構造の前に、デバイス上で行列 a、b、c にアクセスできる必要があります。dispatch 構造から MKL ルーチン cblas_dgemm が呼び出されると、a、b、c に対応するデバイスポインターが MKL ルーチンに引数として渡されるため、計算では a、b、c のデバイスコピーが使用されます。
dispatch ディレクティブでは use_device_ptr 句は必要ありません。oneMKL ルーチンに必要なデバイスポイン
ターのリストは、GPU バリアント関数が宣言されている oneMKL OpenMP* オフロード・ヘッ ダー・ファイル
mkl_omp_offload.h で指定されています。ユーザーは、oneMKL ヘッダーファイルで必要なデバイスポインターのリストを確認し、oneMKL ルーチンを呼び出す前に、対応する行列がデバイスからアクセス可能であることを確認する必要があります。
target variant dispatch ディレクティブ (非推奨)
oneMKL の計算を GPU にオフロードする非推奨の方法は、インテル固有の OpenMP* 拡張 target variant
dispatch ディレクティブを使用することです。次の例に示すように、target variant dispatch 構造内に
oneMKL ルーチンの呼び出しを配置します。
#pragma omp target data map(to: A[0:m*k], B[0:k*n]) map(tofrom: C[0:m*n]){#pragma omp target variant dispatch use_device_ptr(A, B, C){

195

oneAPI GPU 最適化ガイド
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A, k, B, n, beta, C, n);}}

上記の例では、dispatch 構造の前に、デバイス上で行列 a、b、c にアクセスできる必要があります。target
variant dispatch ディレクティブには、a、b、c がデバイス上に対応するストレージを持つオブジェクトを指していることを示すため use_device_ptr(A,B,C) 句が必要です。cblas_dgemm が呼び出されると、a、b、c に対応するデバイスポインターが引数として渡され、a、b、c のデバイスコピーが計算に使用されます。
注









dispatch (または target variant dispatch) を使用して oneMKL 計算を GPU にオフロードする場合、
oneMKL ルーチンは計算を開始する前にデバイス上で配列/行列にアクセスできることを想定しています。そのため、ユーザーは、行列 a、b、c をデバイスにマップするか、デバイス上で直接行列を割り当てるか、または
oneMKL ルーチンを呼び出す前に統合共有メモリー (USM) に行列を割り当てる必要があります。メモリー割り
当ての詳細については、OpenMP* のメモリー割り当てを参照してください。
oneMKL ルーチンが dispatch (または target variant dispatch) 構造から呼び出されない場合、またはオフロードが無効になっている場合、oneMKL 計算は CPU 上で実行されます。
OpenMP* dispatch (または target variant dispatch) 構造から発行できる oneMKL ルーチンへの呼び出しは 1 つだけです。oneMKL ルーチンへの呼び出しが 2 回連続してある場合は、呼び出しを別々のdispatch (または target variant dispatch) 構造に配置する必要があります。
use_device_ptr 句は、dispatch ディレクティブでは必要ありません。ただし、ポインターがデバイス上に対応するストレージを持つオブジェクトを指していることを示すために、target variant dispatch ディレク
ティブでは必要です。
使用しているコンパイラのバージョンによっては、dispatch ディレクティブを受け入れるためコンパイラー・オ
プション -fopenmp-version=51 を追加する必要がある場合があります。

FortranFortran コードから oneMKL ルーチンを呼び出す場合は、次の include 文を必ず追加する必要があります。
include "mkl_omp_offload.f90"

また、oneMKL Fortran API を 32 ビット整数で呼び出す場合は、次のモジュールの use 文を追加します。
use onemkl_blas_omp_offload_lp64

一方、oneMKL Fortran API を 64 ビット整数で呼び出す場合は、次のモジュールの use 文を追加します。
use onemkl_blas_omp_offload_ilp64

次の Fortran の例では、Fortran プログラムから DGEMM を呼び出す方法と、前述の include 文と use 文について説明しています。
!$omp target data map(to: a, b) map(tofrom: c2)!$omp dispatch
call DGEMM('N','N',m,n,k,alpha,a,m,b,k,beta,c2,m)!$omp end target data

196

oneAPI GPU 最適化ガイド
上記の Fortran プログラムを 32 ビット整数でコンパイルおよびリンクするには、次のコマンドを使用します。
$ ifx -fiopenmp -fopenmp-targets=spir64 -qmkl -fpp -free -c dgemm_dispatch_f.f90$ ifx -fiopenmp -fopenmp-targets=spir64 -qmkl -fsycl -L${MKLROOT}/lib/intel64 -liomp5-lsycl -lOpenCL -lstdc++ -lpthread -lm -ldl -lmkl_sycl dgemm_dispatch_f.o

上記の Fortran プログラムを 64 ビット整数でコンパイルおよびリンクするには、次のコマンドを使用します。
$ ifx -fiopenmp -fopenmp-targets=spir64 -qmkl -m64 -DMKL_ILP64 -i8 -fpp -free -cdgemm_dispatch_f.f90$ ifx -fiopenmp -fopenmp-targets=spir64 -qmkl -fsycl -L${MKLROOT}/lib/intel64 -liomp5-lsycl -lOpenCL -lstdc++ -lpthread -lm -ldl -lmkl_sycl dgemm_dispatch_f.o

C/C++ または Fortran プログラムから実行ファイル (a.out) を生成し、その実行ファイルを unitrace で実行すると、生成されたトレースファイルから「Device Timing Results」という見出しを検索します。その見出しの下に、
oneMKL カーネルがリストされているはずです。このように、oneMKL 計算が GPU にオフロードされていることを確認できます。
実行コマンドの例:$ export OMP_TARGET_OFFLOAD=MANDATORY$ export ZE_AFFINITY_MASK=0.0$ unitrace -h -d ./a.out

5-15-5-3

oneMKL GEMM 呼び出しのバッチ処理

oneMKL ライブラリーには、複数の oneMKL 呼び出しを 1 つの呼び出しにまとめる「バッチ」ルーチンが含まれています。実行時に oneMKL は、全体のパフォーマンスを最適化するため、すべての行列操作をインテリジェントに実行します。
例えば、cblas_dgemm ルーチンは、2 つの一般行列 a と b の「行列-行列積」を計算して、結果を行列 c に返します。
cblas_dgemm のインターフェイスを以下に示します。
void cblas_dgemm (const CBLAS_LAYOUT layout,
const CBLAS_TRANSPOSE transa, const CBLAS_TRANSPOSE transb,
const MKL_INT m, const MKL_INT n, const MKL_INT k,const double alpha, const double *a,const MKL_INT lda, const double *b,
const MKL_INT ldb, const double beta,
double *c, const MKL_INT ldc);

cblas_dgemm_batch ルーチンは、cblas_dgemm ルーチンと似ていますが、cblas_dgemm_batch ルーチンは行列のグループを使用して「行列-行列操作」を行い一度に複数のグループを処理します。
cblas_dgemm_batch のインターフェイスを以下に示します。cblas_dgemm インターフェイスと似ていることに注意してください。ただし、行列の引数は行列へのポインターの配列として渡し、パラメーターはパラメーターの配列として渡します。
void cblas_dgemm_batch (const CBLAS_LAYOUT layout,
const CBLAS_TRANSPOSE* transa_array, const CBLAS_TRANSPOSE* transb_array,
const MKL_INT* m_array, const MKL_INT* n_array, const MKL_INT* k_array,
const double* alpha_array, const double **a_array,
const MKL_INT* lda_array, const double **b_array,
const MKL_INT* ldb_array, const double* beta_array,double **c_array, const MKL_INT* ldc_array,
const MKL_INT group_count, const MKL_INT* group_size);197

oneAPI GPU 最適化ガイド
バッチ操作は次のように定義されています。
idx = 0for i = 0 .. group_count - 1
alpha_array[i] と beta_array[i] の alpha および beta
for j = 0 .. group_size[i] - 1
a, b, c 行列は、それぞれ a_array[idx], b_array[idx], and c_array[idx] にありますc := alpha*op(a)*op(b) + beta*c,
idx = idx + 1
end forend for

説明:
•
•
•
•
•
•
•

op(X) は、op(X) = X、op(X) = XT、または op(X) = XH のいずれか
alpha と beta は、alpha_array と beta_array のスカラー要素
a、b、および c は、m_array、n_array、そして k_arrayare の要素である m、n、k の行列
op(a) は m-by-k matrix
op(b) は k-by-n matrix
C は m-by-n matrix
a、b、c は、それぞれ a_array、b_array、c_array が指すアドレスに格納されている行列を表します。
a_array、b_array、および c_array のエントリー数は、total_batch_count に等しい group_size エ
ントリーの合計です

異なる形状とパラメーターの乗算をグループにパッケージ化することでバッチ処理できます。各グループは、同じ形状 (同じ m、n、k) と同じパラメーターの行列乗算で構成されます。
バッチ API の基本前提は、バッチ内のすべての操作は (同一グループでも異なるグループでも) 互いに独立していることです。そのため、oneMKL はバッチ内の操作間の順序付けは保証せず、複数の操作を並列に実行します。
一般に、バッチサイズが大きくなるほど良い結果をもたらします。これにより、oneMKL は操作をより適切に並列化し、GPU 全体にワークを分散できます。
2 回の cblas_dgemm 呼び出しを、1 回の cblas_dgemm_batch 呼び出しに置き換える方法を説明します。次の例
では、cblas_dgemm を 2 回呼び出しています。
#pragma omp target data\map(to: A1[0:m*k], B1[0:k*n], A2[0:m*k], B2[0:k*n]) \map(tofrom: C1[0:m*n], C2[0:m*n]){#pragma omp dispatch
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A1, k, B1, n, beta, C1, n);#pragma omp dispatch
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A2, k, B2, n, beta, C2, n);}

上記の cblas_dgemm の 2 つの呼び出しは、次の例に示すように 1 つの cblas_dgemm_batch に置き換え、一括してバッチ処理できます。同一パラメーター (layout、transa、transb、m、n、k、alpha、lda、ldb、beta、およびldc) で 2 回の行列乗算を行っているため、バッチはサイズ 2 の 1 つのグループで構成されていることに注意してください。ここで total_batch_size は 2 です。

198

oneAPI GPU 最適化ガイド// Call cblas_dgemm_batch#pragma omp target enter data \map(to: A1[0:m*k], B1[0:k*n], C1[0:m*n]) \map(to: A2[0:m*k], B2[0:k*n], C2[0:m*n])#pragma omp target data use_device_ptr(A1, B1, C1, A2, B2, C2){
a_array[0] = A1, a_array[1] = A2;
b_array[0] = B1, b_array[1] = B2;
c_array[0] = C1, c_array[1] = C2;}#pragma omp target datamap(to:a_array[0:2], b_array[0:2], c_array[0:2]){#pragma omp dispatch
cblas_dgemm_batch (CblasRowMajor,transa_array,transb_array,m_array,n_array,k_array,alpha_array,(const double **)a_array,lda_array,(const double **)b_array,ldb_array,beta_array,c_array,ldc_array,group_count,group_sizes);} // end target data map\#pragma omp target exit data \map(from: C1[0:m*n], C2[0:m*n])

ここで使用する GPU (1 スタックのみ) で実行した 2 つの例のパフォーマンスは次のようになります。
dgemm_example_01_c.cpp (two calls to cblas_dgemm): 2.976183 seconds
dgemm_batch_example_01_c.cpp (one call to cblas_dgemm_batch): 1.881641 seconds

さらに複雑なバッチの例を以下に示します。この例では、3 つのグループで構成されるバッチ (GROUP_COUNT=3) があります。各グループのサイズは、1 から 10 の間でランダムに選択されます。いくつかのパラメーター (layout、
transA、transB、m、n、および k) がランダムに選択されますが、各グループではすべての乗算で同じパラメーターが適用されます。total_batch_size はすべてのグループサイズの合計と等価です。
double *a, *b, *c;for (i = 0; i < total_batch_size; i++) {a = a_array[i];b = b_array[i];c = c_array[i];#pragma omp target enter datamap(to:a[0:sizea_array[i]],b[0:sizeb_array[i]],c[0:sizec_array[i]])#pragma omp target data use_device_ptr(a,b,c){a_array_dev[i] = a;b_array_dev[i] = b;c_array_dev[i] = c;}}#pragma omp target data map(to:a_array_dev[0:total_batch_size], \b_array_dev[0:total_batch_size], \c_array_dev[0:total_batch_size]) device(dnum){

199

oneAPI GPU 最適化ガイド#pragma omp dispatch
cblas_dgemm_batch(layout, transA, transB, m, n, k, alpha, (const double **)a_array_dev, lda, (const double **) b_array_dev, ldb, beta, c_array_dev, ldc,
GROUP_COUNT, group_size);}for (i = 0; i < total_batch_size; i++) {a = a_array[i];b = b_array[i];c = c_array[i];#pragma omp target exit datamap(from:a[0:sizea_array[i]],b[0:sizeb_array[i]],c[0:sizec_array[i]])}5-15-5-4

独立した連続する GEMM 呼び出しの高速化

独立して実行できる連続した GEMM 呼び出しの実行を高速化する方法はいろいろあります。1 つは、上記のようにバッチ版の GEMM を呼び出して GEMM 呼び出しをバッチ処理する方法です。
もう 1 つは、GEMM 呼び出しを OpenMP* parallel 構造で囲み、並列領域を実行する各 OpenMP* スレッドがGEMM 呼び出しを 1 つずつディスパッチする方法です。この並列化の方法を次の例で説明します。
#pragma omp target data\map(to: A1[0:m*k], B1[0:k*n], A2[0:m*k], B2[0:k*n])map(tofrom: C1[0:m*n], C2[0:m*n]){#pragma omp parallel num_threads(2){int id = omp_get_thread_num();\if (id == 0) {#pragma omp dispatch
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A1, k, B1, n, beta, C1, n);}else if (id == 1) {#pragma omp dispatch
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A2, k, B2, n, beta, C2, n);}}}

独立して連続した GEMM 呼び出しの実行を高速化するもう 1 つの方法は、ホストスレッドが次の GEMM 呼び出しをディスパッチする前に、ディスパッチされた GEMM 呼び出しの完了を待機しないよう、dispatch 構造で nowait 節を使用する方法です。最後の GEMM 呼び出しの後に OpenMP* taskwait ディレクティブを挿入して、ホストスレッ
ドが処理を続ける前に、ディスパッチされたすべての oneMKL 呼び出しが完了することを保証します。この nowaitを使用したアプローチを次に示します。
#pragma omp target data\map(to: A1[0:m*k], B1[0:k*n], A2[0:m*k], B2[0:k*n]) \map(tofrom: C1[0:m*n], C2[0:m*n]){#pragma omp dispatch nowait
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A1, k, B1, n, beta, C1, n);#pragma omp dispatch nowait
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A2, k, B2, n, beta, C2, n);200

oneAPI GPU 最適化ガイド#pragma omp taskwait}

5-15-5-5

GEMM 計算で使用される行列のパディング

GEMM 呼び出しは、行列 a、b、c の先頭の次元 lda、ldb、ldc をそれぞれパディングすることで高速化できます。
行列の先頭の次元は、メモリー内の行列レイアウトによって異なります。
••

行優先レイアウト (C/C++): 行列の主次元は、行列の列です。
列優先レイアウト (Fortran): 行列の主次元は、行列の行です。

DGEMM 呼び出しに渡す行列サイズには次の 2 つの留意事項があります。
規則 1: 最高のパフォーマンスを得るには、GEMM 呼び出しに渡す行列の最初の次元を 64 バイトの倍数 (フル・キャッシュライン・サイズ) にする必要があります。単精度データでは、先頭の次元が （６４/sizeof(float)) の倍数の 16 であることを意味し、倍精度データでは、先頭の次元が （６４/sizeof(double)) の倍数、つまり 8 であることを意味します。
可能であれば、すべての行列 (a、b、c) をパディングします。ただし、行列 c のパディングは、a と b ほど重要でありません。
規則 2: 最高のパフォーマンスを得るには、先頭の次元を 2 の乗数の倍数にしてはなりません (4096 など)。先頭の次元を上記の条件より少し大きくすると (例: 4096 から 4096 + 64 バイトなど)、パフォーマンスが向上する場合があります。
パディングの例 (Fortran)
次の Fortran の例は、DGEMM 呼び出しに渡す行列をパディングして、パフォーマンスが向上する方法を示しています。
1. include "mkl_omp_offload.f90"
2.
3.
! このサブルーチンはコマンドライン引数 m1、k1、n1 を読み取ります4.
subroutine get_arguments (m1, k1, n1)
5.
implicit none
6.
integer:: m1, k1, n1
7.
character(len=32) :: m1_char, k1_char, n1_char
8.
9.
! コマンドライン引数の数が正しいことを確認
10.
!
11.
if (command_argument_count() .ne. 3) then12.
print *, "ERROR: Three command-line arguments expected; stopping."
13.
stop
14.
endif
15.
16.
! コマンドライン引数を取得
17.
call get_command_argument(1, m1_char)
18.
call get_command_argument(2, k1_char)
19.
call get_command_argument(3, n1_char)
20.
21.
! 引数を整数に変換
22.
read (m1_char,*) m1
23.
read (k1_char,*) k1
24.
read (n1_char,*) n1
25.
end subroutine get_arguments
26.

201

oneAPI GPU 最適化ガイド
27.
28.

! この関数は、>= n である 8 の倍数の最小値を返します

29.
! 例:
30.
! if n = 3, then get_mul8 = 831.
! if n = 9, then get_mul8 = 1632.
! if n = 30, then get_mul8 = 32
33.
! if n = 80, then get_mul8 = 834.
integer function get_mul8 (n)35.
implicit none
36.
integer :: n37.
integer :: mod38.
if (mod(n,8) .eq. 0) then39.
get_mul8 = n
40.
else
41.
get_mul8 = ((n/8) + 1) * 8
42.
endif
43.
end function get_mul8
44.
45.
46.
! このサブルーチンで行列を初期化
47.
subroutine init_matrix (m, k, n, a, b, c)
48.
implicit none49.
integer:: m, k, n
50.
double precision :: a(m,k), b(k,n), c(m,n)
51.
integer:: i, j52.
53.
do i = 1, m
54.
do j = 1, k
55.
a(i,j) = (i-1) - (0.25 * k)56.
end do57.
end do58.
59.
do i = 1, k
60.
do j = 1, n61.
b(i,j) = -((i-1) + j)
62.
end do63.
end do64.
65.
do i = 1, m
66.
do j = 1, n
67.
c(i,j) = 0.2 + i - j
68.
end do69.
end do70.
end subroutine init_matrix
71.
72.
73.
program DGEMM_MAIN
74.
75. #if defined(MKL_ILP64)
76.
use onemkl_blas_omp_offload_ilp64
77. #else78.
use onemkl_blas_omp_offload_lp64
79. #endif80.
use omp_lib
81.
use iso_fortran_env
82.
implicit none
83.
84.
interface85.
integer function get_mul8 (n)86.
implicit none87.
integer :: n
88.
end function get_mul8
89.
end interface
90.
91.
double precision :: alpha, beta
92.
integer :: m1, k1, n1, m2, k2, n2
93.
double precision, allocatable :: a1(:,:)
94.
double precision, allocatable :: b1(:,:)

202

oneAPI GPU 最適化ガイド
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.

double precision, allocatable :: c1(:,:)
double precision, allocatable :: a2(:,:)
double precision, allocatable :: b2(:,:)
double precision, allocatable :: c2(:,:)
double precision :: start_t1, end_t1
double precision :: start_t2, end_t2! コマンドライン引数 m1、k1、n1 の読み取り
call get_arguments (m1, k1, n1)!
! alpha、beta、m2、k2、n2 を初期化!
alpha = 1.025
beta = 0.75
m2 = get_mul8(m1)k2 = get_mul8(k1)n2 = get_mul8(n1)!
! 行列の割り当てと初期化。
!
allocate( a1(1:m1,1:k1) )allocate( b1(1:k1,1:n1) )allocate( c1(1:m1,1:n1) )allocate( a2(1:m2,1:k2) )allocate( b2(1:k2,1:n2) )allocate( c2(1:m2,1:n1) )call init_matrix (m1, k1, n1, a1, b1, c1)
call init_matrix (m2, k2, n2, a2, b2, c2)!$omp target data map(to: a1, b1, a2, b2) map(tofrom: c1, c2)! デバイスをウォームアップ実行!$omp dispatch
call DGEMM('N','N',m1,n1,k1,alpha,a1,m1,b1,k1,beta,c1,m1)!
! デバイスで DGEMM を実行 (使用するパディングメトリックは a1、b1、c1)!
start_t1 = omp_get_wtime()!$omp dispatch
call DGEMM('N','N',m1,n1,k1,alpha,a1,m1,b1,k1,beta,c1,m1)
end_t1 = omp_get_wtime()! デバイスをウォームアップ実行!$omp dispatch
call DGEMM('N','N',m2,n2,k2,alpha,a2,m2,b2,k2,beta,c2,m2)!
! デバイスで DGEMM を実行 (使用するパディングメトリックは a2、b2、c2)!
start_t2 = omp_get_wtime()!$omp dispatch
call DGEMM('N','N',m2,n2,k2,alpha,a2,m2,b2,k2,beta,c2,m2)

203

oneAPI GPU 最適化ガイド
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173. 100
174. 101
175. 111
176. 102
177. 112
178.
179.

end_t2 = omp_get_wtime()!$omp end target data
print 100, alpha, beta
print *
print 101, m1, n1, k1
print 111, (end_t1 - start_t1)
print *
print 102, m2, n2, k2
print 112, (end_t2 - start_t2)
format(7x, "ALPHA =", f10.4, " BETA =",f10.4)
format(7x, "M1 =", i5," N1 =", i5, " K1 =",i5)format(7x, "Time (non-padded arrays) =", f10.4, " sec")
format(7x, "M2 =", i5," N2 =", i5, " K2 =",i5)format(7x, "Time (padded arrays) =", f10.4, " sec")end

上記の例では、配列境界 (m1、k1、n1) がコマンドライン引数として渡されています。行列 a1(1:m1, 1:k1)、b1(1:k1, 1:n1)、c1(1:m1, 1:n1) が割り当てられて初期化されます。
また、パディングされた行列 a2(1:m2, 1:k2)、b2(1:k2, 1:n2)、c1(1:m2, 1:n2) が割り当てられて初期化されます。m2 は >= m1 である 8 の最小倍数です。同様に、k2 は <= k1 である 8 の最小倍数で、n2 は >= n1 である 8 の最小倍数です。
プログラムは、a1、b1、c1 の DGEMM 計算に要した時間と、a2、b2、c2 の DGEMM 計算に要した時間を比較します。
このプログラムのコンパイルとリンク、および実行コマンドを以下に示します。
コンパイル:$ ifx -fiopenmp -fopenmp-targets=spir64 -qmkl -fpp -free -c dgemm_pad_f_01.f
リンク:$ ifx -fiopenmp -fopenmp-targets=spir64 -qmkl -lOpenCL dgemm_pad_f_01.o
実行:$ expoet ZE_AFFINITY_MASK=0.0$ export LIBOMPTARGET_DEBUG=0$ ./a.out 12001 12001 12001

特定の GPU (1 スタックのみ) で実行した出力を示します。
ALPHA =
1.0250 BETA =0.7500M1 =12001 N1 =12001 K1 =12001
Time (non-padded arrays) =0.2388 sec
M2 =12008 N2 =12008 K2 =12008
Time (padded arrays) =0.1648 sec

上記では、配列 a、b、c をパディングすることで、DGEMM 呼び出しにかかる時間を、0.2388 秒から 0.1648 秒に短縮できました。

5-15-5-6

参考資料

1.

『インテル® oneAPI マス・カーネル・ライブラリー・デベロッパー・リファレンス - C』 (英語)2.

『インテル® oneAPI マス・カーネル・ライブラリー・デベロッパー・リファレンス - Fortran』 (英語)204oneAPI GPU 最適化ガイド
3.

『インテル® oneAPI マス・カーネル・ライブラリー・デベロッパー・リファレンス – Fortran』 - 「新しい行列引数タイプ」 (英語)

4.

バッチ GEMM 操作の紹介 (英語)5.

インテル® oneAPI マス・カーネル・ライブラリーのリンクライン・アドバイザー (英語)

5-15-6OpenMP* アプリケーションのパフォーマンスを解析するツール

OpenMP* プログラムのパフォーマンスを解析し、ボトルネックを特定するのに役立つさまざまなツールやメカニズ
ムが提供されています。
•

インテル® VTune™ プロファイラー
インテル® VTune™ プロファイラーを使用して、OpenMP* アプリケーションのパフォーマンスを解析できます。
アプリケーションの中で最も時間を費やす (ホットな) 関数を特定し、アプリケーションが CPU 依存であるか、GPU 依存であるか、またコードを GPU にどれだけ効率良くオフロードできるか、シーケンシャルとスレッド化のパフォーマンスを高めるため最適化すべきコード領域などを特定するのを支援します。インテル® VTune™ プ
ロファイラーの詳細については、『インテル® VTune™ プロファイラーのユーザーガイド』をご覧ください。

•

レベルゼロと OpenCL* トレースツール
unitrace は、SYCL* および OpenMP* GPU オフロードに対応したレベルゼロ・バックエンド向けのホストおよびデバイスのトレースツールです。unitrace を -h -d オプションで使用すると、トレースの最後にホスト側とデバイス側のサマリーを、「API Timing Results」と「Device Timing Results」という項目に出力します。ト
レースには、明示的なデータ転送のみが出力されることに注意してください。USM (統合共有メモリー) に割り当てられたデータ転送がトレースに含まれないことがあります。

注:•

unitrace は 、 oneMKL カ ー ネ ル が オ フ ロ ー ド さ れ た こ と を 確 認 す る の に 便 利 で す 。
OMP_TARGET_OFFLOAD=MANDATORY 環境変数は oneMKL に影響しないため、oneMKL カーネルがオフロードされたことを確認するには利用できません。oneMKL カーネル (および他のカーネル) がオフロードされたことを確認する方法の 1 つは、unitrace が生成するトレースの「Device Timing Results」にリストされるカーネルを確認することです。

SYCL_PI_TRACE=2 環境変数: SYCL* のランタイム・プラグイン・インターフェイス (PI) は、SYCL* ランタイムのデバ
イスに依存しない部分と、デバイス上での実行を制御するデバイス固有のランタイムレイヤー間とのインターフェイ
ス・レイヤーです。SYCL_PI_TRACE=2 に設定すると、すべての PI 呼び出しと引数および戻り値のトレースを取得できます。詳細については、「DPC++ ランタイム環境変数」を参照してください。
LIBOMPTARGET_DEBUG=1 環境変数: LIBOMPTARGET_DEBUG 環境変数は、libomptarget.so からのデバッグ
情報の取得を制御します。
デバッグ出力は、このドキュメントの「多くの GPU リソースを使用するデータ転送とメモリー割り当てを最小化する」の項で示すように、ループ反復の ND-range のパーティション化、ホストとデバイス間のデータ転送、メモリー使用などに関する有用な情報を提供します。
LIBOMPTARGET_DEBUG 環境変数の詳細については、LLVM/OpenMP* ランタイム (英語) を参照してください。

205

oneAPI GPU 最適化ガイド
LIBOMPTARGET_PLUGIN_PROFILE 環境変数: LIBOMPTARGET_PROFILE 環境変数は、libomptarget.so が時間プロファイル出力を生成するのを制御します。詳細については、LLVM/OpenMP* ランタイム (英語) を参照してください。
デバイスのコンパイラーで生成されたアセンブリーのダンプ: ジャストインタイム (JIT) コンパイルを実行する前に(または事前コンパイル (AOT) ではプログラムを実行する前に）、次の 2 つの環境変数を設定することでコンパイラーが生成するアセンブリーをダンプできます。
export IGC_ShaderDumpEnable=1
export IGC_DumpToCustomDir=my_dump_dir

LLVM IR、アセンブリー、および GenISA ファイルは、my_dump_dir (または指定する名前) のサブ・ディレクトリーにダンプされます。このサブ・ディレクトリーに、各カーネルの *.asm ファイルがあります。ファイル名は、カーネルが起動されるソースの行番号を示します。ファイルのヘッダーには、SIMD 幅やコンパイルオプションなどの情報が含まれています。
また、my_dump_dir には HardwareCaps.txt というファイルがあり、ベクトルエンジン数、スレッド数、スライス数など GPU に関連する情報が含まれます。
インテル® グラフィックス・コンパイラーの詳細と、コンパイルを制御するフラグ (環境変数) の一覧は、インテル® グラ
フィックス・コンパイラーの Linux* 向け OpenCL* 設定フラグ (英語) を参照してください。
デバッグとプロファイルの詳細については、このドキュメントの「パフォーマンスのプロファイルと解析」の項を参照してください。

5-15-7OpenMP* オフロードの最良の事例

ここでは、GPU にオフロードするアプリケーションのパフォーマンスを向上させる最良の事例を紹介します。事例を以下のカテゴリーに分類して、以降のセクションで説明します。
•
•
•
•
•
•
•
•

多くの GPU リソースを使用する
データ転送とメモリー割り当てを最小化する
OpenMP* 構造を有効活用する
メモリー割り当て
Fortran の例
節: is_device_ptr、use_device_ptr、has_device_addr、use_device_addr
プリフェッチSLM とアトミック

注:
OpenMP* のパフォーマンスを収集するため次の構成を使用しました。
•
•
•
•
•
•

206

2 スタックのインテル® GPU
単一の GPU タイルのみ (暗黙的または明示的なスケーリングなし)
インテル® コンパイラー、ランタイム、および GPU ドライバー
レベルゼロプラグイン
起動時のオーバーヘッドを計測しないようにダミーの target 構造を挿入
ジャストインタイム (JIT) コンパイルモードを使用

oneAPI GPU 最適化ガイド

5-15-7-1

多くの GPU リソースを使用する

並列に実行できる work-item 数を多くして、より多くの GPU リソースを利用する (GPU を処理で一杯にする) ことで、オフロードされたコードのパフォーマンスを向上できます。
注:•

ループ反復の ND-Range への分割は、コンパイラーとランタイムのヒューリスティックによって決定されますが、GPU ドライバーとハードウェア構成にも依存します。そのため、時間経過とともに変化する可能性があります。しかし、LIBOMPTARGET_DEBUG=1 の出力からパーティショニングを割り出す方法は変わりません。

5-15-7-1-1

collapse 節

入れ子になったループの並列性を高める方法として、collapse 節を使用して入れ子になった 2 階層以上のループ
を 1 つのループとして融合する方法があります。1 つのループに融合することで、並列に実行できる反復回数が増加
し、GPU でより多くの work-item を処理できます。
次の例では、4 つの入れ子になったループを GPU にオフロードしています。parallel for ディレクティブは、一番外側のループ (30 行目) を並列実行することを指示しています。ループの反復数は BLOCKS であり 8 が設定されています。
1. #include <math.h>
2. #include <omp.h>
3. #define P 16
4. #define BLOCKS 8
5. #define SIZE (BLOCKS * P * P * P)
6. #define MAX 100
7. #define scaled_rand() ((rand() % MAX) / (1.0 * MAX))
8. #define IDX2(i, j) (i * P + j)
9. #define IDX4(b, i, j, k) (b * P * P * P + i * P * P + j * P + k)
10. int main(void) {11.
double w[SIZE];/* 出力 */
12.

double u[SIZE], dx[P * P]; /* 入力 */13.

int b, i, j, k, l;

14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.

double start, end;/* タイマー */omp_set_default_device(0);/* 起動時のオーバーヘッドを計測しないよう、ダミーの target 領域を追加しています */#pragma omp target{ ; }/* 乱数値で入力を初期化します */srand(0);for (int i = 0; i < SIZE; i++)u[i] = scaled_rand();for (int i = 0; i < P * P; i++)dx[i] = scaled_rand();/* デバイスへデータをマップ */#pragma omp target enter data map(to: u[0:SIZE], dx[0:P * P])
start = omp_get_wtime();/* collapse 節なしでカーネルをオフロードします */#pragma omp target teams distribute parallel for private(b, i, j, k, l)for (b = 0; b < BLOCKS; b++) {for (i = 0; i < P; i++) {for (j = 0; j < P; j++) {for (k = 0; k < P; k++) {double ur = 0.;
double us = 0.;
double ut = 0.;/* ループカウンター */207

oneAPI GPU 最適化ガイド
37.
for (l = 0; l < P; l++) {38.
ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];39.
us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];40.
ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];
41.
}
42.
w[IDX4(b, i, j, k)] = ur * us * ut;
43.
}
44.
}
45.
}
46.
}
47.
end = omp_get_wtime();48.
#pragma omp target exit data map(from: w[0:SIZE])
49.
/* 結果を出力します */50.
printf("no-collapse-clause: w[0]=%lf time=%lf\n", w[0], end - start);51.
return 0;
52. }コンパイルコマンド$ icx -fiopenmp -fopenmp-targets=spir64 test_no_collapse.cpp

実行コマンド$ OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0.0 LIBOMPTARGET_DEBUG=1./a.out

libomptarget.so のデバッグ情報 (環境変数 LIBOMPTARGET_DEBUG=1 の時に実行時に出力される) には、ルー
プ反復の ND-range パーティション化と collapse 節を使用した際の並列性の向上が示されています。出力の Lbと Ub は、パーティション化された各次元の並列ループの上限と下限を示します。
collapse 節がないと、LIBOMPTARGET_DEBUG=1 による出力は、50 行目 target 領域に対しの次のような情報を示します。
Libomptarget --> Launching target execution __omp_offloading_3d_9b5f515d__Z4main_l45
with pointer 0x000000000143d5d8 (index=1).
Target LEVEL0 RTL --> Executing a kernel 0x000000000143d5d8...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 7, Stride = 1
Target LEVEL0 RTL --> Group sizes = {1, 1, 1}
Target LEVEL0 RTL --> Group counts = {8, 1, 1}

collapse 節がない場合、最も外側のループの上限は (BLOCKS) = 8 であるため、並列ループの反復回数は 8 になります。この場合、1 つの work-group が 8 つの work-item を持ちます (work-group の合計は 1 x 1 x 1 = 1 で、各 work-group のサイズは 8 x 1 x 1 = 8 work-item)。カーネルは SIMD32 でベクトル化されており、32 個のwork-item が 1 つの sub-group にまとめられています。work-group ごとに ｗork-item が 1 つしかないため、各
work-group には 1 つの SIMD レーンのみがアクティブな sub-group も 1 つしかないことになります。
parallel for ディレクティブに collapse 節を追加することで、work-item の数を増やして GPU の並列性を高めることができます。最初に、次の例に示すように collapse(2) 節を追加してみます。
/* collapse 節を指定してカーネルをオフロードします */#pragma omp target teams distribute parallel for collapse(2) private(b, i, j, k, l)for (b = 0; b < BLOCKS; b++) {for (i = 0; i < P; i++) {for (j = 0; j < P; j++) {for (k = 0; k < P; k++) {double ur = 0.;
double us = 0.;
double ut = 0.;

208

oneAPI GPU 最適化ガイド

for (l = 0; l < P; l++) {ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];}w[IDX4(b, i, j, k)] = ur * us * ut;}}}}

LIBOMPTARGET_DEBUG=1 による出力は、collapse(2) が指定された場合のパーティション化の情報を次のように示します。
Libomptarget --> Launching target execution __omp_offloading_3d_9b5f515f__Z4main_l45
with pointer 0x00000000017f45d8 (index=1).
Target LEVEL0 RTL --> Executing a kernel 0x00000000017f45d8...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 15, Stride = 1
Target LEVEL0 RTL --> Level 1: Lb = 0, Ub = 7, Stride = 1
Target LEVEL0 RTL --> Group sizes = {1, 1, 1}
Target LEVEL0 RTL --> Group counts = {16, 8, 1}

collapse(2) の場合、並列ループの反復回数は BLOCKS x P = 8 x 16 = 128 となります。この場合、最終的に
work-group は 128 になり、各 work-group には 1 個の work-item があります (work-group の合計は 16 x 8 x
1 = 128 で、各 work-group のサイズは 1 x 1 x 1 = 1 work-item です)。カーネルは SIMD32 でベクトル化されており、32 個の work-item が 1 つの sub-group にまとめられています。つまり work-group ごとに ｗork-item が 1 つしかないため、各 work-group には 1 つの SIMD レーンのみがアクティブが sub-group が 1 つしかないことになります。
一方、collapse(3) 節にすると、LIBOMPTARGET_DEBUG=1 の出力は次のようなパーティション化の情報を示します。
Libomptarget --> Launching target execution __omp_offloading_3d_9b5f5160__Z4main_l45
with pointer 0x0000000001728d08 (index=1).
Target LEVEL0 RTL --> Executing a kernel 0x0000000001728d08...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 15, Stride = 1
Target LEVEL0 RTL --> Level 1: Lb = 0, Ub = 15, Stride = 1
Target LEVEL0 RTL --> Level 2: Lb = 0, Ub = 7, Stride = 1
Target LEVEL0 RTL --> Group sizes = {8, 1, 1}
Target LEVEL0 RTL --> Group counts = {2, 16, 8}

collapse(3) の場合、並列ループの反復数は BLOCKS x P x P = 8 x 16 x 16 = 2048 となります。この場合、最終的に work-group は 256 になり、各 work-group には 8 個の work-item があります (work-group の合計は 2 x
16 x 8 = 256 で、各 work-group のサイズは 8 x 1 x 1 = 8 work-item です)。カーネルは SIMD32 でベクトル化されており、32 個の work-item が 1 つの sub-group にまとめられています。work-group ごとに 8 つの work-item
しかないため、8 つの SIMD レーンがアクティブな sub-group は 1 つしかないことになります。
collapse(3) 節の代わりに collapse(4) 節を使用すると、LIBOMPTARGET_DEBUG=1 の出力は次のようなパーティション化情報を示します。
Target LEVEL0 RTL --> Executing a kernel 0x0000000001aab5d8...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64

209

oneAPI GPU 最適化ガイド
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 32767, Stride = 1
Target LEVEL0 RTL --> Group sizes = {64, 1, 1}
Target LEVEL0 RTL --> Group counts = {512, 1, 1}

collapse(4) の場合、並列ループの反復回数は BLOCKS x P x P x P = 8 x 16 x 16 x 16= 32768 となります。この場合、最終的に work-group は 512 になり、各 work-group には 64 個の work-item があります (work-groupの合計は 512 x 1 x 8 = 512 で、各 work-group のサイズは 64 x 1 x 1 = 64 work-item です)。カーネルは
SIMD32 でベクトル化されており、32 個の work-item が 1 つの sub-group にまとめられています。つまり、
各 work-group は 2 つの sub-group を持つことになります。
collapse 節を使用すると、入れ子になったループの実行時間が大幅に短縮されます。ここで使用する GPU (1 ス
タックのみ) で実行した各バージョンのパフォーマンスは次のようになります。
no collapse version :0.002430 seconds
collapse(2) version :0.000839 seconds
collapse(3) version : 0.000321 seconds
collapse(4) version : 0.000325 seconds

上記の結果から、collapse(3) 節または collapse(4) 節を追加すると、パフォーマンスがおよそ 7.5 倍向上した
ことが分かります (0.000321 秒 vs 0.002430 秒)。
注:•

GPU 上では collapse 節で実際のループを融合できないかもしれませんが、この節はコンパイラーとランタイ
ムに入れ子になったループの並列性の度合いを伝え、ND-range のパーティション化に利用されることに留意してください。
ベクトルロードとストアの利点を活用するため、入れ子になったループの最も内側のループは、ベクトル化できるように collapse で融合しないことを推奨します。最も内側のループがユニットストライドで、反復数がSIMD 幅と同じになるよう十分に大きければ、最良のパフォーマンスを得ることができます。

•

5-15-7-2

データ転送とメモリー割り当てを最小化する

GPU に計算をオフロードする場合、ホストとデバイス間のデータ転送を最小にして、デバイス上のメモリー割り当てを減らすことが重要です。これには、次に示すようにいくつかの方法があります。

5-15-7-2-1

target enter data と target exit data ディレクティブを使用する

変数が複数の target 構造で使用される場合、target enter data と target exit data ディレクティブで
コード領域を囲むことで、ホストとデバイス間のデータ転送を最小限にすることができます。
ホストからデバイスにデータを転送する最初の target 構造の前に target enter data ディレクティブを配置し、デバイスからホストにデータを転送する最後の target 構造の後に target exit data ディレクティブを配置します。
次の例では、2 つの target 構造 (30 行目と 50 行目) があり、それぞれの target 構造は配列 dx と u を読み取り、
配列 w に書き込むことを想定しています。
1.
2.
3.
4.
5.
6.

210

#include <stdio.h>#include <stdlib.h>#include <time.h>#include <math.h>#include <omp.h>#define P 16

oneAPI GPU 最適化ガイド
7. #define BLOCKS 8
8. #define SIZE (BLOCKS * P * P * P)
9. #define MAX 100
10. #define scaled_rand() ((rand() % MAX) / (1.0 * MAX))
11. #define IDX2(i, j) (i * P + j)
12. #define IDX4(b, i, j, k) (b * P * P * P + i * P * P + j * P + k)
13. int main(void) {14.
double w[SIZE];/* 出力 */
15.

double u[SIZE], dx[P * P]; /* 入力 */16.

int b, i, j, k, l;

17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.

double start, end;/* タイマー */omp_set_default_device(0);/* 起動時のオーバーヘッドを計測しないよう、ダミーの target 領域を追加しています */#pragma omp target{ ; }/* 乱数値で入力を初期化します */srand(0);for (int i = 0; i < SIZE; i++)u[i] = scaled_rand();for (int i = 0; i < P * P; i++)dx[i] = scaled_rand();start = omp_get_wtime();/* カーネル #1 をオフロード */#pragma omp target teams distribute parallel for collapse(4) \map(to: u[0:SIZE], dx[0:P * P]) map(from: w[0:SIZE]) private(b, i, j, k, l)for (b = 0; b < BLOCKS; b++) {for (i = 0; i < P; i++) {for (j = 0; j < P; j++) {for (k = 0; k < P; k++) {double ur = 0.;
double us = 0.;
double ut = 0.;
for (l = 0; l < P; l++) {ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];}w[IDX4(b, i, j, k)] = ur * us * ut;}}}}/* カーネル #2 をオフロード */#pragma omp target teams distribute parallel for collapse(4) \map(to: u[0:SIZE], dx[0:P * P]) map(tofrom: w[0:SIZE]) private(b, i, j, k, l)for (b = 0; b < BLOCKS; b++) {for (i = 0; i < P; i++) {for (j = 0; j < P; j++) {for (k = 0; k < P; k++) {double ur = b + i + j - k;double us = b + i + j - k;double ut = b + i + j - k;for (l = 0; l < P; l++) {ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];}w[IDX4(b, i, j, k)] += ur * us * ut;}}}}
end = omp_get_wtime();/* 結果を出力します */printf("target region: w[0]=%lf time=%lf\n", w[0], end - start);/* ループカウンター */211

oneAPI GPU 最適化ガイド
72.
return 0;
73. }コンパイルコマンド$ icx-cl -fiopenmp -fopenmp-targets=spir64 test_no_target_enter_exit_data.cpp

実行コマンド$ export OMP_TARGET_OFFLOAD=MANDATORY$ export ZE_AFFINITY_MASK=0.0$ export LIBOMPTARGET_DEBUG=1./a.out

最初の target 構造 (30 行目) に到達するとランタイムは次のように処理します。
••

配列 dx と u は map 節で to マップタイプとして指定されているため、デバイス上に dx と u のストレージが割り当てられ、ホスト上の dx と u の値が対応するデバイスの配列にコピーされます。
配列 w は map 節で from マップタイプとして指定されているため、デバイス上には初期化されない配列 w が割り当てられます。

最初の target 構造の最後で次のことが行われます。
•

配列 w は map 節で from マップタイプとして指定されているため、デバイス上の配列 w の値はホスト上の配列
w にコピーされます。

2 番目の target 構造 (50 行目) に到達するとランタイムは次のように処理します。
•

配列 dx と u は map 節で to マップタイプとして指定されているため、デバイス上に dx と u のストレージが割り当てられ、ホスト上の dx と u の値が対応するデバイスの配列にコピーされます。

2 番目の target 構造の最後で次のことが行われます。
•

配列 w は map 節で from マップタイプとして指定されているため、デバイス上の配列 w の値はホスト上の配列
w にコピーされます。

次の LIBOMPTARGET_DEBUG=1 の出力は、両方の target 構造 (30 行目と 50 行目) で同じデータがパーティショ
ン化されていることを示しています。
Libomptarget --> Launching target execution __omp_offloading_3d_15ece5c8__Z4main_l42
with pointer 0x00000000024cb5d8 (index=1).
Target LEVEL0 RTL --> Executing a kernel 0x00000000024cb5d8...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 32767, Stride = 1
Target LEVEL0 RTL --> Group sizes = {64, 1, 1}
Target LEVEL0 RTL --> Group counts = {512, 1, 1}
Target LEVEL0 RTL --> Executing a kernel 0x0000000002b9c5e0...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 32767, Stride = 1
Target LEVEL0 RTL --> Group sizes = {64, 1, 1}
Target LEVEL0 RTL --> Group counts = {512, 1, 1}
Target LEVEL0 RTL --> Kernel Pointer argument 0 (value: 0xff00ffffffee0000) was set
successfully for device 0.

212

oneAPI GPU 最適化ガイド(両方の target 領域で) 転送されたデータ量は、LIBOMPTARGET_DEBUG=1 の出力を grep "Libomptarget -> Moving" を実行して確認できます。
$ grep "Libomptarget --> Moving" test_no_target_enter_exit_data.debug
Libomptarget --> Moving 2048 bytes (hst:0x00007fff60f05030) ->(tgt:0xff00fffffffe0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007fff60ec5030) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00fffffff20000) ->(hst:0x00007fff60e85030)
Libomptarget --> Moving 2048 bytes (hst:0x00007fff60f05030) ->(tgt:0xff00fffffffe0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007fff60ec5030) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007fff60e85030) ->(tgt:0xff00fffffff20000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00fffffff20000) ->(hst:0x00007fff60e85030)

次の修正例に示すように、target enter data と target exit data ディレクティブを使用することで、ホストからデバイス、またはデバイスからホストへのデータコピーを減らすことができます。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <time.h>
4. #include <math.h>
5. #include <omp.h>
6. #define P 16
7. #define BLOCKS 8
8. #define SIZE (BLOCKS * P * P * P)
9. #define MAX 100
10. #define scaled_rand() ((rand() % MAX) / (1.0 * MAX))
11. #define IDX2(i, j) (i * P + j)
12. #define IDX4(b, i, j, k) (b * P * P * P + i * P * P + j * P + k)
13. int main(void) {14.
double w[SIZE];/* 出力 */
15.

double u[SIZE], dx[P * P]; /* 入力 */16.

int b, i, j, k, l;

17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.

double start, end;/* タイマー */omp_set_default_device(0);/* 起動時のオーバーヘッドを計測しないよう、ダミーの target 領域を追加しています */#pragma omp target{ ; }/* 乱数値で入力を初期化します */srand(0);for (int i = 0; i < SIZE; i++)u[i] = scaled_rand();for (int i = 0; i < P * P; i++)dx[i] = scaled_rand();start = omp_get_wtime();/* データをデバイスにマップします。w はデフォルトの map(tofrom: w[0:SIZE])/* ループカウンター */にはせず、map(alloc: w[0:SIZE]) にします */#pragma omp target enter data map(to: u[0:SIZE], dx[0:P * P]) \map(alloc: w[0:SIZE])/* カーネル #1 をオフロード */#pragma omp target teams distribute parallel for collapse(4) private(b, i, j, k,l)35.
for (b = 0; b < BLOCKS; b++) {36.
for (i = 0; i < P; i++) {37.
for (j = 0; j < P; j++) {38.
for (k = 0; k < P; k++) {39.
double ur = 0.;
30.
31.
32.
33.
34.

213

oneAPI GPU 最適化ガイド
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.

double us = 0.;
double ut = 0.;
for (l = 0; l < P; l++) {ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];}w[IDX4(b, i, j, k)] = ur * us * ut;}}}}/* カーネル #2 をオフロード */#pragma omp target teams distribute parallel for collapse(4) private(b, i, j, k,l)54.
for (b = 0; b < BLOCKS; b++) {55.
for (i = 0; i < P; i++) {56.
for (j = 0; j < P; j++) {57.
for (k = 0; k < P; k++) {58.
double ur = b + i + j - k;
59.
double us = b + i + j - k;
60.
double ut = b + i + j - k;
61.
for (l = 0; l < P; l++) {62.
ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];63.
us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];64.
ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];
65.
}
66.
w[IDX4(b, i, j, k)] += ur * us * ut;
67.
}
68.
}
69.
}
70.
}
71.
#pragma omp target exit data map(from: w[0:SIZE])
72.
end = omp_get_wtime();73.
/* 結果を出力します */74.
printf("target region: w[0]=%lf time=%lf\n", w[0], end - start);75.
return 0;
76. }この例では、target enter data ディレクティブ (31 行目) に到達すると次のことが行われます。
••

配列 dx と u は map 節で to マップタイプとして指定されているため、デバイス上に dx と u のストレージが割り当てられ、ホスト上の dx と u の値が対応するデバイスの配列にコピーされます。
配列 w は map 節で alloc マップタイプとして指定されているため、デバイス上には初期化されない配列 w が割り当てられます。

最初の target 構造 (34 行目) に到達するとランタイムは次のように処理します。
•

ランタイムは、配列 dx、u、および w がデバイスに存在するか確認します。存在するとデータ転送は行われません。

最初の target 構造の最後で次のことが行われます。
•

ランタイムは、配列 dx、u、および w のストレージがデバイス上に保持されると想定し、デバイスからホストへのコピーバックは行いません。

2 番目の target 構造 (53 行目) に到達するとランタイムは次のように処理します。
•
214

ここでもホストからデバイスへのデータ転送は行われません。

oneAPI GPU 最適化ガイド
2 番目の target 構造の最後で次のことが行われます。
•

ランタイムは、配列 dx、u、および w のストレージがデバイス上に保持されると想定し、デバイスからホストへのコピーバックは行いません。

target exit data ディレクティブ (71 行目) に到達すると次のことが行われます。
•

配列 w は map 節で from マップタイプとして指定されているため、デバイス上の配列 w の値はホスト上の配列
w にコピーされます。

target enter data と target exit data ディレクティブを使用することで、特定の GPU (1 スタックのみ) の
実行時間が短縮されました。
No target enter/exit data version : 0.001204 seconds
target enter/exit data version: 0.000934 seconds

LIBOMPTARGET_DEBUG=1 の出力は、データのパーティション化が両方の例 (target enter data と target
exit data あり/なし) で同じであることを示しています。
Libomptarget --> Looking up mapping(HstPtrBegin=0x00007ffd899939c0, Size=2048)...
Libomptarget --> Mapping exists with HstPtrBegin=0x00007ffd899939c0,
TgtPtrBegin=0xff00ffffffee0000, Size=2048, DynRefCount=2 (update suppressed),
HoldRefCount=0
Libomptarget --> Obtained target argument (Begin: 0xff00ffffffee0000, Offset: 0) fromhost pointer 0x00007ffd899939c0
Libomptarget --> Looking up mapping(HstPtrBegin=0x00007ffd899539c0, Size=262144)...
Libomptarget --> Mapping exists with HstPtrBegin=0x00007ffd899539c0,
TgtPtrBegin=0xff00ffffffef0000, Size=262144, DynRefCount=2 (update suppressed),
HoldRefCount=0
Libomptarget --> Obtained target argument (Begin: 0xff00ffffffef0000, Offset: 0) fromhost pointer 0x00007ffd899539c0
Libomptarget --> Looking up mapping(HstPtrBegin=0x00007ffd899139c0, Size=262144)...
Libomptarget --> Launching target execution __omp_offloading_3d_fadb4d__Z4main_l47
with pointer 0x0000000002b9c5d8 (index=1).
Target LEVEL0 RTL --> Executing a kernel 0x0000000002b9c5d8...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 32767, Stride = 1
Target LEVEL0 RTL --> Group sizes = {64, 1, 1}
Target LEVEL0 RTL --> Group counts = {512, 1, 1}

target enter data と target exit data によるパフォーマンスの向上は、データ転送の削減によるものであり、ここでは次の 3 つのデータ転送が行われています。
$ grep "Libomptarget --> Moving" test_target_enter_exit_data.debug
Libomptarget --> Moving 262144 bytes (hst:0x00007ffd899539c0) ->(tgt:0xff00ffffffef0000)
Libomptarget --> Moving 2048 bytes (hst:0x00007ffd899939c0) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00fffffff30000) ->(hst:0x00007ffd899139c0)

215

oneAPI GPU 最適化ガイド

5-15-7-2-2

マップタイプの選択

パフォーマンスを向上するには、マップされる変数のマップタイプを、その変数の target 構造での利用状況と一致させることが重要です。
次の例では、配列 u と dx は target 構造では読み取りのみであり、配列 w は target 構造で書き込まれています。
しかし、これらの変数のマップタイプはすべて tofrom に指定されており、非効率です。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <time.h>4.
5. #include <math.h>
6. #include <omp.h>
7.
8. #define P 16
9. #define BLOCKS 8
10. #define SIZE (BLOCKS * P * P * P)
11.
12. #define MAX 100
13. #define scaled_rand() ((rand() % MAX) / (1.0 * MAX))
14.
15. #define IDX2(i, j) (i * P + j)
16. #define IDX4(b, i, j, k) (b * P * P * P + i * P * P + j * P + k)
17.
18. int main(void) {19.
double w[SIZE];/* 出力 */
20.

double u[SIZE], dx[P * P]; /* 入力 */21.

int b, i, j, k, l;/* ループカウンター */

22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.

double start, end;/* タイマー */216

omp_set_default_device(0);/* 起動時のオーバーヘッドを計測しないよう、ダミーの target 領域を追加しています */#pragma omp target{ ; }/* 乱数値で入力を初期化します */srand(0);for (int i = 0; i < SIZE; i++)u[i] = scaled_rand();for (int i = 0; i < P * P; i++)dx[i] = scaled_rand();start = omp_get_wtime();#pragma omp target teams distribute parallel for \private(b, i, j, k, l) \map(tofrom: u[0:SIZE], dx[0:P * P]) \map(tofrom: w [0:SIZE])
for (int n = 0; n < SIZE; n++) {k = n - (n / P) * P;
j = (n - k) / P;i = (n - (j * P + k)) / (P * P);b = n / (P * P * P);
double ur = 0.;
double us = 0.;
double ut = 0.;
for (l = 0; l < P; l++) {ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];

oneAPI GPU 最適化ガイド
56.
us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];57.
ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];
58.
}
59.
60.
w[IDX4(b, i, j, k)] = ur * us * ut;
61.
}
62.
63.
end = omp_get_wtime();64.
65.
printf("offload: w[0]=%lf time=%lf\n", w[0], end - start);
66.
67.
return 0;
68. }コンパイルコマンド$ icpx -fiopenmp -fopenmp-targets=spir64 test_map_tofrom.cpp

実行コマンド$ OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0.0 LIBOMPTARGET_DEBUG=1./a.out

パフォーマンスを向上するには、次に示すように u と dx のマップタイプを to に w のマップタイプは from にする必要があります。
#pragma omp target teams distribute parallel for \private(b, i, j, k, l) \map(to: u[0:SIZE], dx[0:P * P]) \map(from: w [0:SIZE])
for (int n = 0; n < SIZE; n++) {k = n - (n / P) * P;
j = (n - k) / P;i = (n - (j * P + k)) / (P * P);b = n / (P * P * P);
double ur = 0.;
double us = 0.;
double ut = 0.;
for (l = 0; l < P; l++) {ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];}w[IDX4(b, i, j, k)] = ur * us * ut;}

より明確なマップタイプ (tofrom ではなく to または from) を使用することで、特定の GPU (1 スタックのみ) での実
行時間を短縮できました。
tofrom map-types version: 0.001141 seconds
to or from map-types version : 0.000908 seconds

LIBOMPTARGET_DEBUG=1 の出力は、u、dx、および w に tofrom マップタイプを指定すると、ホストとデバイス間で不要なデータ転送があることを示しています。tofrom マップタイプでは、u、dx、w の値がホストからデバイスに、またデバイスからホストに合計 6 回コピーされます。

217

oneAPI GPU 最適化ガイド$ grep "Libomptarget --> Moving" test_map_tofrom.debug
Libomptarget --> Moving 2048 bytes (hst:0x00007fff1f6ad540) ->(tgt:0xff00fffffffe0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007fff1f66d540) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007fff1f62d540) ->(tgt:0xff00fffffff20000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00fffffff20000) ->(hst:0x00007fff1f62d540)
Libomptarget --> Moving 262144 bytes (tgt:0xff00ffffffee0000) ->(hst:0x00007fff1f66d540)
Libomptarget --> Moving 2048 bytes (tgt:0xff00fffffffe0000) ->(hst:0x00007fff1f6ad540)

より明確なマップタイプ (to または from) を指定することで、u と dx の値をホストからデバイスにコピーする 2 回の転送と、w の値をデバイスからホストにコピーする 1 回の転送の合計 3 回のデータ転送に抑えることができます。
$ grep "Libomptarget --> Moving" test_map_to_or_from.debug
Libomptarget --> Moving 2048 bytes (hst:0x00007fffc2258fd0) ->(tgt:0xff00fffffffe0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007fffc2218fd0) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00fffffff20000) ->(hst:0x00007fffc21d8fd0)

5-15-7-2-3

読み取り専用のスカラー変数をマップしない

コンパイラーは、target 構造の読み取り専用スカラー変数がマップされず、target 構造の firstprivate 節に指定されるか、もしくはどの節にも指定されていない場合、最も効率の良いコードを生成できます。スカラー変数がtarget 構造のいずれの節にも指定されない場合、デフォルトは firstprivate になることに注意してください。
読み取り専用のスカラー変数を map(to:) 節に指定すると、デバイス上で不要なメモリー割り当てやホストからデバ
イスへのデータ転送が発生します。一方、target 構造で読み取り専用のスカラー変数を firstprivate として指定すると、カーネル起動時に引数として渡され、変数のメモリー割り当てやコピーは必要ありません。
次の例では、入れ子になったループを GPU にオフロードしています。target 構造の map(to:) 節には、読み取り専
用の 3 つのスカラー変数 s1、s2、s3 が指定されています。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <time.h>4.
5. #include <math.h>
6. #include <omp.h>
7.
8. #define P 16
9. #define BLOCKS 8
10. #define SIZE (BLOCKS * P * P * P)
11.
12. #define MAX 100
13. #define scaled_rand() ((rand() % MAX) / (1.0 * MAX))
14.
15. #define IDX2(i, j) (i * P + j)
16. #define IDX4(b, i, j, k) (b * P * P * P + i * P * P + j * P + k)
17.
18. int main(void) {19.
double w[SIZE];/* 出力 */
20.

double u[SIZE], dx[P * P]; /* 入力 */21.

double s1, s2, s3;/* スカラー */22.

int b, i, j, k, l;/* ループカウンター */218

oneAPI GPU 最適化ガイド
23.
double start, end;/* タイマー */24.
25.
omp_set_default_device(0);26.
27.
/* 起動時のオーバーヘッドを計測しないよう、ダミーの target 領域を追加しています */28.
#pragma omp target
29.
{ ; }
30.
31.
/* 乱数値で入力を初期化します */32.
srand(0);33.
for (int i = 0; i < SIZE; i++)34.
u[i] = scaled_rand();35.
36.
for (int i = 0; i < P * P; i++)37.
dx[i] = scaled_rand();38.
39.
/* スカラーの初期化 */40.
s1 = u[SIZE / 2];41.
s2 = scaled_rand();42.
s3 = 0.145;
43.
44.
/* デバイスへデータをマップ */45.
#pragma omp target enter data map(to: u[0:SIZE], dx[0:P * P])
46.
47.
start = omp_get_wtime();48.
49.
/* collapse 節を指定してカーネルをオフロードします */50.
#pragma omp target teams distribute parallel for collapse(4) \51.
map(to: s1, s2, s3) private(b, i, j, k, l)52.
for (b = 0; b < BLOCKS; b++) {53.
for (i = 0; i < P; i++) {54.
for (j = 0; j < P; j++) {55.
for (k = 0; k < P; k++) {56.
double ur = 0.;57.
double us = 0.;58.
double ut = 0.;59.
60.
for (l = 0; l < P; l++) {61.
ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)] + s1;
62.
us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)] - s2;
63.
ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)] * s3;
64.
}
65.
66.
w[IDX4(b, i, j, k)] = ur * us * ut;
67.
}
68.
}
69.
}
70.
}
71.
72.
end = omp_get_wtime();73.
74.
#pragma omp target exit data map(from: w[0:SIZE])
75.
76.
/* 結果を出力します */77.
printf("collapse-clause: w[0]=%lf time=%lf\n", w[0], end - start);
78.
79.
return 0;
80. }コンパイルコマンド$ icpx -fiopenmp -fopenmp-targets=spir64 test_scalars_map.cpp

219

oneAPI GPU 最適化ガイド
実行コマンド$ OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0.0 LIBOMPTARGET_DEBUG=1 ./a.out

次の例に示すように、target 構造の firstprivate 節で s1、s2、および s3 を指定するか、どの節にも指定しないほうが効率的です。
/* collapse 節を指定してカーネルをオフロードします */#pragma omp target teams distribute parallel for collapse(4) \firstprivate(s1, s2, s3) private(b, i, j, k, l)for (b = 0; b < BLOCKS; b++) {for (i = 0; i < P; i++) {for (j = 0; j < P; j++) {for (k = 0; k < P; k++) {double ur = 0.;
double us = 0.;
double ut = 0.;
for (l = 0; l < P; l++) {ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)] + s1;
us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)] - s2;
ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)] * s3;}
w[IDX4(b, i, j, k)] = ur * us * ut;}}}}

map(to:s1, s2, s3) の代わりに firstprivate(s1, s2, s3) を使用すると、特定の GPU (1 スタックのみ)
での実行時間が短縮できました。
map(to:s1,s2,s3) バージョン : 0.001324 seconds
firstprivate(s1,s2,s3) バージョン : 0.000730 seconds

LIBOMPTARGET_DEBUG=1 の出 力では、 データの パ ーティシ ョン化の 両方の 例 map(to:s1, s2, s3) とfirstprivate(to:s1, s2, s3) で同じであることが分かります。
Libomptarget --> Launching target execution __omp_offloading_3d_9b49d7d8__Z4main_l51
with pointer 0x0000000002b295d8 (index=1).
Target LEVEL0 RTL --> Executing a kernel 0x0000000002b295d8...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 32767, Stride = 1
Target LEVEL0 RTL --> Group sizes = {64, 1, 1}
Target LEVEL0 RTL --> Group counts = {512, 1, 1}
Libomptarget --> Launching target execution __omp_offloading_3d_9b49d7dd__Z4main_l51
with pointer 0x0000000001f475d8 (index=1).
Target LEVEL0 RTL --> Executing a kernel 0x0000000001f475d8...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 32767, Stride = 1
Target LEVEL0 RTL --> Group sizes = {64, 1, 1}
Target LEVEL0 RTL --> Group counts = {512, 1, 1}

map(to:s1, s2, s3) 節を指定すると、デバイスメモリー割り当てとメモリー転送が発生します。

220

oneAPI GPU 最適化ガイド
次の LIBOMPTARGET_DEBUG=1 の出力は、map(to:s1, s2, s3) 節を使用した際のデバイス上のメモリー割り
当てに関連する情報を示しています。
Target LEVEL0 RTL --> Memory usage for device memory, device 0x000000000278e470
Target LEVEL0 RTL --> -- Allocator:
Native,Pool
Target LEVEL0 RTL --> -- Requested:
1179648,526360Target LEVEL0 RTL --> -- Allocated:
1179648,526528Target LEVEL0 RTL --> -- Freed:
1179648,262336
Target LEVEL0 RTL --> -- InUse:
0,264192
Target LEVEL0 RTL --> -- PeakUse :1179648,526528Target LEVEL0 RTL --> -- NumAllocs:
3,
6

割り当てられたメモリーは 1,179,648 バイトであり、(Pool からの) 割り当て数は 3 つの配列 (dx、u、w) と 3 つのス
カラー (s1、s2、s3) の 6 つであることに注意してください。
一方、firstprivate(s1, s2, s3) 節を指定すると LIBOMPTARGET_DEBUG=1 の出力は、デバイスメモリーの割り当てが減少していることを示しています。以下に示すように、割り当てメモリーサイズは、 1,179,648 から
1,114,112 バイトに減少し (64 K バイト)、Pool からの割り当て数は 6 から 3 に減少しています。
Target LEVEL0 RTL --> Memory usage for device memory, device 0x0000000001bab440
Target LEVEL0 RTL --> -- Allocator:
Native,Pool
Target LEVEL0 RTL --> -- Requested:
1114112,526336
Target LEVEL0 RTL --> -- Allocated:
1114112,526336
Target LEVEL0 RTL --> -- Freed:
1114112,262144
Target LEVEL0 RTL --> -- InUse:
0,264192
Target LEVEL0 RTL --> -- PeakUse :1114112,526336
Target LEVEL0 RTL --> -- NumAllocs:
2,
3

また、map(to:) 節を指定すると、メモリー割り当てが増加するだけでなく、ホストからデバイスへのデータ転送がさらに多くなります。これは、LIBOMPTARGET_DEBUG=1 の出力に、grep "Libomptarget --> Moving" コマンドを実行すると分かります。
$ grep "Libomptarget --> Moving" test_scalars_map.debug
Libomptarget --> Moving 262144 bytes (hst:0x00007ffdf5526760) ->(tgt:0xff00ffffffef0000)
Libomptarget --> Moving 2048 bytes (hst:0x00007ffdf5566760) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 8 bytes (hst:0x00007ffdf55670a0) -> (tgt:0xff00ffffffed0000)
Libomptarget --> Moving 8 bytes (hst:0x00007ffdf55670a8) -> (tgt:0xff00ffffffed0040)
Libomptarget --> Moving 8 bytes (hst:0x00007ffdf55670b0) -> (tgt:0xff00ffffffed0080)
Libomptarget --> Moving 262144 bytes (hst:0x00007ffdf54e6760) ->(tgt:0xff00fffffff30000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00fffffff30000) ->(hst:0x00007ffdf54e6760)

また、firstprivate(to:s1, s2, s3) 節を指定すると LIBOMPTARGET_DEBUG=1 の出力は次のようになります。
$ grep "Libomptarget --> Moving" test_scalars_fp.debug
Libomptarget --> Moving 262144 bytes (hst:0x00007ffda809c4a0) ->(tgt:0xff00ffffffef0000)
Libomptarget --> Moving 2048 bytes (hst:0x00007ffda80dc4a0) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007ffda805c4a0) ->(tgt:0xff00fffffff30000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00fffffff30000) ->(hst:0x00007ffda805c4a0)

map(to:s1, s2, s3) の例では、さらの 3 つのデータが転送 (それぞれ 8 バイト) されていることに注意してください。この転送では、s1、s2、および s3 の値がホストからデバイスにコピーされます。
221

oneAPI GPU 最適化ガイド

5-15-7-2-4

ループ境界をマップしないことで適切な ND-Range のパーティション化が可能

前述し たように、target 構造内の読み取り専用のスカラー変数がマップされない、 または target 構造のfirstprivate 節に指定されるか、どの節にも指定されていない場合、コンパイラーは効率良いコードを生成できます。
これは、スカラー変数が target 構造の並列ループの境界にある場合、特に顕著です。ループ境界 (下限、上限、またはステップ) のいずれかがマップされている場合、デバイス上で不要なメモリー割り当てや、ホストからデバイスへの
データコピーが発生します。これはループのパーティション化でも影響を受け、不適切な ND-range のパーティション化が発生してパフォーマンスを損ねる可能性があります。
次の例を考えてみます。ここでは parallel for ループを GPU にオフロードしています。for ループの上限はスカラー変数 upper で target 構造でマップされています (48 行目)。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <time.h>4.
5. #include <math.h>
6. #include <omp.h>
7.
8. #define P 16
9. #define BLOCKS 8
10. #define SIZE (BLOCKS * P * P * P)
11.
12. #define MAX 100
13. #define scaled_rand() ((rand() % MAX) / (1.0 * MAX))
14.
15. #define IDX2(i, j) (i * P + j)
16. #define IDX4(b, i, j, k) (b * P * P * P + i * P * P + j * P + k)
17.
18. int main(void) {19.
double w[SIZE];/* 出力 */
20.

double u[SIZE], dx[P * P]; /* 入力 */

21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.

int b, i, j, k, l;
int upper;
double start, end;

222/* ループカウンター *//* タイマー */omp_set_default_device(0);/* 起動時のオーバーヘッドを計測しないよう、ダミーの target 領域を追加しています */#pragma omp target{ ; }/* 乱数値で入力を初期化します */srand(0);for (int i = 0; i < SIZE; i++)u[i] = scaled_rand();for (int i = 0; i < P * P; i++)dx[i] = scaled_rand();upper = (int)dx[0] + SIZE;/* デバイスへデータをマップ */#pragma omp target enter data map(to: u[0:SIZE], dx[0:P * P])
start = omp_get_wtime();oneAPI GPU 最適化ガイド
45.
46.
/* カーネルをオフロード */47.
#pragma omp target teams distribute parallel for private(b, i, j, k, l) \48.
map(to: upper)49.
for (int n = 0; n < upper; n++) {50.
double ur = 0.;51.
double us = 0.;52.
double ut = 0.;53.
54.
k = n - (n / P) * P;55.
j = (n - k) / P;56.
i = (n - (j * P + k)) / (P * P);
57.
b = n / (P * P * P);
58.
59.
for (l = 0; l < P; l++) {60.
ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];61.
us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];62.
ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];
63.
}
64.
65.
w[IDX4(b, i, j, k)] = ur * us * ut;
66.
}
67.
68.
end = omp_get_wtime();69.
70.
/* デバイスからデータをマップします */71.
#pragma omp target exit data map(from: w[0:SIZE])
72.
73.
printf("offload: w[0]=%lf time=%lf\n", w[0], end - start);
74.
75.
return 0;
76. }コンパイルコマンド$ icpx -fiopenmp -fopenmp-targets=spir64 test_loop_bounds_map.cpp

実行コマンド$ OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0.0 LIBOMPTARGET_DEBUG=1 ./a.out

upper はマップされているため、ホスト上の変数 upper の値はデバイス上の値と異なる可能性があります。そのため、実行時に target 領域をオフロードすると、ホストでオフロードしたループの反復回数を知ることができません。
その場合、ランタイム (libomptarget.so) は、デバイスとカーネルのプロパティーから、GPU 全体を使用するように ND-Range のパーティション化を行います。
コンパイラーが生成するオフロードループのコードには、オフロードループの内側に最内ループ (work-item ごと) が含まれます。グローバルサイズが実際のループの反復回数よりも小さい場合、各 work-item は元のループの反復回数を処理します。逆に、グローバルサイズが実際のループの反復回数よりも大きい場合、いくつかの work-item はワークを処理しません。コンパイラーが生成するループ内の if 条件は、これをチェックし残りのループ本体をスキップします。
上記の例 (upper が マップ) の LIBOMPTARGET_DEBUG=1 の出力は、次のように ND-range のパーティション化を示します。
Libomptarget --> Launching target execution __omp_offloading_3d_1ff4bf1c__Z4main_l48
with pointer 0x00000000021175d8 (index=1).
Target LEVEL0 RTL --> Executing a kernel 0x00000000021175d8...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64

223

oneAPI GPU 最適化ガイド
Target LEVEL0 RTL --> Group sizes = {1024, 1, 1}
Target LEVEL0 RTL --> Group counts = {512, 1, 1}

上記のパーティション化による work-item の総数は、512 x 1024 = 524,288 であり、実際のループの反復回数よりも大きく (32,767 回) なっています。いくつかの work-item は何のワークも処理しません。
ホスト側でオフロードされたループの反復回数が分かっていれば、さらに適切な ND-Range のパーティション化が可能です。これにより、コンパイラーとランタイムは、ループの反復回数に合わせて RD-Range をパーティション化できます。
適 切 な パ ー テ ィ シ ョ ン 化 を 行 う に は 、 次 の 例 に 示 す よ う に target 構 造 の map(to:upper) の 代 わ り に
firstprivate(upper) を使用します。この方法では、コンパイラーはホスト上の upper 変数の値がデバイス上のupper 変数と同じであることを認識できます。
#pragma omp target teams distribute parallel for private(b, i, j, k, l) \firstprivate(upper)for (int n = 0; n < upper; n++) {double ur = 0.;
double us = 0.;
double ut = 0.;k = n - (n / P) * P;
j = (n - k) / P;i = (n - (j * P + k)) / (P * P);b = n / (P * P * P);
for (l = 0; l < P; l++) {ur += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];us += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];ut += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];}w[IDX4(b, i, j, k)] = ur * us * ut;}

修正した例 (upper が firstprivate) の LIBOMPTARGET_DEBUG=1 の出力は、次のように ND-range のパーティション化を示します。
Libomptarget --> Launching target execution __omp_offloading_3d_1fed0edf__Z4main_l48
with pointer 0x00000000029b3d08 (index=1).
Target LEVEL0 RTL --> Executing a kernel 0x00000000029b3d08...
Target LEVEL0 RTL --> Assumed kernel SIMD width is 32
Target LEVEL0 RTL --> Preferred group size is multiple of 64
Target LEVEL0 RTL --> Level 0: Lb = 0, Ub = 32767, Stride = 1
Target LEVEL0 RTL --> Group sizes = {64, 1, 1}
Target LEVEL0 RTL --> Group counts = {512, 1, 1}

上記のパーティション化による work-item の総数は、512 x 64 = 32,767 であり、実際のループの反復回数に正確に一致します。
map(to:upper) の代わりに firstprivate(upper) を使用すると、特定の GPU (1 スタックのみ) の実行時間は次のように短縮されました。
map(to:upper) version: 0.000415 seconds
firstprivate(upper) version :0.000307 seconds

224

oneAPI GPU 最適化ガイド

5-15-7-2-5

デバイス上にメモリーを割り当て

上記に示すように map 節は、元のホスト変数をデバイス上の対応する変数にどのようにマップするかを決定します。
ただし、map(to:) 節は、すべてのケースにおいてデバイス上の変数にメモリーを割り当てる最良の方法であるわけ
ではありません。
次の例では、変数 ur、us、および ut はデバイス上の計算でテンポラリー配列として使用されています。配列は、
map(to:) 節を使用してデバイスにマップされています (45 行 - 47 行)。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <time.h>4.
5. #include <math.h>
6. #include <omp.h>
7.
8. #define P 16
9. #define BLOCKS 8
10. #define SIZE (BLOCKS * P * P * P)
11.
12. #define MAX 100
13. #define scaled_rand() ((rand() % MAX) / (1.0 * MAX))
14.
15. #define IDX2(i, j) (i * P + j)
16. #define IDX4(b, i, j, k) (b * P * P * P + i * P * P + j * P + k)
17.
18. int main(void) {19.
double w[SIZE];/* 出力 */
20.

double u[SIZE], dx[P * P]; /* 入力 */21.

double ur[SIZE], us[SIZE], ut[SIZE]; /* ワーク配列 */22.

int b, i, j, k, l;/* ループカウンター */

23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.

double start, end;/* タイマー */omp_set_default_device(0);/* 起動時のオーバーヘッドを計測しないよう、ダミーの target 領域を追加しています。*/#pragma omp target{ ; }/* 乱数値で入力を初期化します */srand(0);for (int i = 0; i < SIZE; i++)u[i] = scaled_rand();for (int i = 0; i < P * P; i++)dx[i] = scaled_rand();start = omp_get_wtime();/* カーネルをオフロードします */#pragma omp target teams distribute parallel for simd simdlen(16) collapse(4) \map(to:u[0:SIZE],dx[0:P*P]) \map(from:w[0:SIZE])\
map(to:ur[0:SIZE])\
map(to:us[0:SIZE])\
map(to:ut[0:SIZE])\
private(b,i,j,k,l)for (b = 0; b < BLOCKS; b++) {for (i = 0; i < P; i++) {for (j = 0; j < P; j++) {for (k = 0; k < P; k++) {w[IDX4(b, i, j, k)] = 0.;225

oneAPI GPU 最適化ガイド
54.
ur[IDX4(b, i, j, k)] = 0.;55.
us[IDX4(b, i, j, k)] = 0.;56.
ut[IDX4(b, i, j, k)] = 0.;57.
58.
for (l = 0; l < P; l++) {59.
ur[IDX4(b, i, j, k)] += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];60.
us[IDX4(b, i, j, k)] += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];61.
ut[IDX4(b, i, j, k)] += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];
62.
}
63.
64.
w[IDX4(b, i, j, k)] = ur[IDX4(b, i, j, k)] * us[IDX4(b, i, j, k)] *65.
ut[IDX4(b, i, j, k)];
66.
}
67.
}
68.
}
69.
}
70.
71.
end = omp_get_wtime();72.
73.
/* 結果を出力します */74.
printf("collapse-clause: w[0]=%lf time=%lf\n", w[0], end - start);
75.
76.
return 0;
77. }コンパイルコマンド$ icpx -fiopenmp -fopenmp-targets=spir64 test_map_to.cpp

実行コマンド$ OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0.0 LIBOMPTARGET_DEBUG=1 ./a.out

ホストとデバイス間で転送されたデータ量は、LIBOMPTARGET_DEBUG=1 の出力を grep "Libomptarget -->Moving" を実行して確認できます。出力は、配列 ur、us および ut の map(to:) 節が、ホストからデバイスヘ各配
列の 262,144 バイトのの転送が発生していることを示しています。
$ grep "Libomptarget --> Moving" test_map_to.debug
Libomptarget --> Moving 262144 bytes (hst:0x00007fffca630880) ->(tgt:0xff00fffffff30000)
Libomptarget --> Moving 262144 bytes (hst:0x00007fffca670880) ->(tgt:0xff00fffffff70000)
Libomptarget --> Moving 262144 bytes (hst:0x00007fffca6b0880) ->(tgt:0xff00fffffffb0000)
Libomptarget --> Moving 2048 bytes (hst:0x00007fffca770880) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007fffca730880) ->(tgt:0xff00ffffffde0000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00ffffffef0000) ->(hst:0x00007fffca6f0880)

配列 ur、us および ut は、デバイス上で一時的な作業用の配列として使用されるため、これらのデータを転送する意
味はありません。そのため、配列の宣言を declare target と end declare target ディレクティブの間に置くとよいでしょう。これは、配列がデバイスのデータ環境にマップされることを意図しますが、ホストとデバイス間で配列の一貫性を維持するため target update ディレクティブが指定されない限り、配列のデータ転送は発生しません。
次に、この方法の修正例を示します。
1.
2.
3.
4.
5.

226

#include <stdio.h>#include <stdlib.h>#include <time.h>#include <math.h>oneAPI GPU 最適化ガイド
6. #include <omp.h>
7.
8. #define P 16
9. #define BLOCKS 8
10. #define SIZE (BLOCKS * P * P * P)
11.
12. #define MAX 100
13. #define scaled_rand() ((rand() % MAX) / (1.0 * MAX))
14.
15. #define IDX2(i, j) (i * P + j)
16. #define IDX4(b, i, j, k) (b * P * P * P + i * P * P + j * P + k)
17.
18. #pragma omp declare target
19. double ur[SIZE], us[SIZE], ut[SIZE]; /* ワーク配列 */20. #pragma omp end declare target
21.
22. int main(void) {23.
double w[SIZE];/* 出力 */
24.

double u[SIZE], dx[P * P]; /* 入力 */25.

int b, i, j, k, l;/* ループカウンター */

26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.

double start, end;/* タイマー */omp_set_default_device(0);/* 起動時のオーバーヘッドを計測しないよう、ダミーの target 領域を追加しています。*/#pragma omp target{ ; }/* 乱数値で入力を初期化します */srand(0);for (int i = 0; i < SIZE; i++)u[i] = scaled_rand();for (int i = 0; i < P * P; i++)dx[i] = scaled_rand();start = omp_get_wtime();/* カーネルをオフロードします */#pragma omp target teams distribute parallel for simd simdlen(16) collapse(4) \map(to:u[0:SIZE],dx[0:P*P]) \map(from:w[0:SIZE])\
private(b,i,j,k,l)for (b = 0; b < BLOCKS; b++) {for (i = 0; i < P; i++) {for (j = 0; j < P; j++) {for (k = 0; k < P; k++) {w[IDX4(b, i, j, k)] = 0.;ur[IDX4(b, i, j, k)] = 0.;us[IDX4(b, i, j, k)] = 0.;ut[IDX4(b, i, j, k)] = 0.;
for (l = 0; l < P; l++) {ur[IDX4(b, i, j, k)] += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];us[IDX4(b, i, j, k)] += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];ut[IDX4(b, i, j, k)] += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];}w[IDX4(b, i, j, k)] = ur[IDX4(b, i, j, k)] * us[IDX4(b, i, j, k)] *ut[IDX4(b, i, j, k)];
}
}
}
}

227

oneAPI GPU 最適化ガイド
71.
end = omp_get_wtime();72.
73.
/* 結果を出力します */74.
printf("collapse-clause: w[0]=%lf time=%lf\n", w[0], end - start);
75.
76.
return 0;
77. }上記の修正例では、デバイス上の配列 ur、us および ut のメモリー割り当ては行われますが、データ転送は行われません。これは、LIBOMPTARGET_DEBUG=1 の出力に、grep "Libomptarget --> Moving" コマンドを実行
すると分かります。これにより、各配列に対しホストからデバイスへの転送 (262,144 バイト) が見られなくなりました。
$ grep "Libomptarget --> Moving" test_map_to.debug
Libomptarget --> Moving 2048 bytes (hst:0x00007ffc546bfec0) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007ffc5467fec0) ->(tgt:0xff00fffffff30000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00ffffffef0000) ->(hst:0x00007ffc5463fec0)

ホストとデバイス間でデータ転送を行わずにメモリーを確保する別の例として、次のように map(to:) 節の代わりに
map(alloc:) 節を使用します (45 行から 47 行目)。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <time.h>4.
5. #include <math.h>
6. #include <omp.h>
7.
8. #define P 16
9. #define BLOCKS 8
10. #define SIZE (BLOCKS * P * P * P)
11.
12. #define MAX 100
13. #define scaled_rand() ((rand() % MAX) / (1.0 * MAX))
14.
15. #define IDX2(i, j) (i * P + j)
16. #define IDX4(b, i, j, k) (b * P * P * P + i * P * P + j * P + k)
17.
18. int main(void) {19.
double w[SIZE];/* 出力 */
20.

double u[SIZE], dx[P * P]; /* 入力 */21.

double ur[SIZE], us[SIZE], ut[SIZE]; /* ワーク配列 */22.

int b, i, j, k, l;/* ループカウンター */

23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.

double start, end;/* タイマー */228

omp_set_default_device(0);/* 起動時のオーバーヘッドを計測しないよう、ダミーの target 領域を追加しています。*/#pragma omp target{ ; }/* 乱数値で入力を初期化します */srand(0);for (int i = 0; i < SIZE; i++)u[i] = scaled_rand();for (int i = 0; i < P * P; i++)dx[i] = scaled_rand();start = omp_get_wtime();oneAPI GPU 最適化ガイド
40.
41.
/* カーネルをオフロードします */42.
#pragma omp target teams distribute parallel for simd simdlen(16) collapse(4) \43.
map(to:u[0:SIZE],dx[0:P*P]) \44.
map(from:w[0:SIZE])\
45.
map(alloc:ur[0:SIZE])\
46.
map(alloc:us[0:SIZE])\
47.
map(alloc:ut[0:SIZE])\
48.
private(b,i,j,k,l)49.
for (b = 0; b < BLOCKS; b++) {50.
for (i = 0; i < P; i++) {51.
for (j = 0; j < P; j++) {52.
for (k = 0; k < P; k++) {53.
w[IDX4(b, i, j, k)] = 0.;54.
ur[IDX4(b, i, j, k)] = 0.;55.
us[IDX4(b, i, j, k)] = 0.;56.
ut[IDX4(b, i, j, k)] = 0.;57.
58.
for (l = 0; l < P; l++) {59.
ur[IDX4(b, i, j, k)] += dx[IDX2(i, l)] * u[IDX4(b, l, j, k)];60.
us[IDX4(b, i, j, k)] += dx[IDX2(k, l)] * u[IDX4(b, i, l, k)];61.
ut[IDX4(b, i, j, k)] += dx[IDX2(j, l)] * u[IDX4(b, i, j, l)];
62.
}
63.
64.
w[IDX4(b, i, j, k)] = ur[IDX4(b, i, j, k)] * us[IDX4(b, i, j, k)] *65.
ut[IDX4(b, i, j, k)];
66.
}
67.
}
68.
}
69.
}
70.
71.
end = omp_get_wtime();72.
73.
/* 結果を出力します */74.
printf("collapse-clause: w[0]=%lf time=%lf\n", w[0], end - start);
75.
76.
return 0;77. }上記の例では、配列 ur、us および ut は、map(alloc:) 節によりデバイス上に ur、us および ut が割り当てられま
すが、データ転送は行われません。declare target と end declare target を使用した場合と同じです。
$ grep "Libomptarget --> Moving" test_map_alloc.debug
Libomptarget --> Moving 2048 bytes (hst:0x00007ffd46f256c0) ->(tgt:0xff00ffffffee0000)
Libomptarget --> Moving 262144 bytes (hst:0x00007ffd46ee56c0) ->(tgt:0xff00ffffffde0000)
Libomptarget --> Moving 262144 bytes (tgt:0xff00ffffffef0000) ->(hst:0x00007ffd46ea56c0)

ここで使用する GPU (1 スタックのみ) で実行した各バージョンのパフォーマンスは次のようになります。
map(to: ) version: 0.001430 seconds
declare target / end declare target version : 0.000874 seconds
map(alloc: ) version: 0.000991 seconds

5-15-7-3
5-15-7-3-1

OpenMP* 構造を有効活用する
nowait を使用して同期を軽減

適切な場合は、target 構造で nowait 節を使用して同期を排除します。

229

oneAPI GPU 最適化ガイド
デフォルトでは、target 領域の終わりに暗黙のバリアがあり、target 構造に到達したホストスレッドは、target
領域が完了するまで待機します。
target 構造に nowait 節を追加すると暗黙のバリアがなくなり、target 構造に到達したホストスレッドは、
target 領域が完了を待たずに処理を続行します。これにより、ホストスレッドは target 領域の完了を待機しない
ため、target 領域はデバイス上で非同期に実行できます。
次の例では、2 つの行列 v1 と v2 の積を parallel 領域で計算するケースを考えてみます (45 行目)。計算の半分は、ホスト上で parallel 領域を実行するスレッドのチームで行われます。残りの半分の計算は、デバイス上で実行されます。チームのプライマリー・スレッドは、デバイス上で計算を行うため target 領域を起動します。
デフォルトでは、チームのプライマリー・スレッドは、target 領域の完了を待機してから、ホストでの計算 (for ルー
プのワークシェア) を続行します。
1. /*2.
* This test is taken from OpenMP API 5.0.1 Examples (June 2020)3.
* https://www.openmp.org/wp-content/uploads/openmp-examples-5-0-1.pdf
4.
* (4.13.2 nowait Clause on target Construct)
5.
*/
6.
7.
8. #include <stdio.h>
9. #include <stdlib.h>
10. #include <time.h>
11. #include <omp.h>
12.
13. #define N 100000 // N は偶数
14.
15. void init(int n, float *v1, float *v2) {16.
int i;
17.
18.
for(i=0; i<n; i++){19.
v1[i] = i * 0.25;20.
v2[i] = i - 1.25;
21.
}
22. }
23.
24. int main() {25.
int i, n=N;
26.
float v1[N],v2[N],vxv[N];27.
double start,end; // timers28.
29.
init(n, v1, v2);
30.
31.
/* 開始時間を測定しないよう、ダミーの parallel と target 領域を
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.

230

追加します。*/#pragma omp parallel{#pragma omp master#pragma omp target{;}}
start=omp_get_wtime();#pragma omp parallel{#pragma omp master#pragma omp target teams distribute parallel formap(to: v1[0:n/2])
map(to: v2[0:n/2])
map(from: vxv[0:n/2])\\\

oneAPI GPU 最適化ガイド
49.
50.
51.
52.

for(i=0; i<n/2; i++){vxv[i] = v1[i]*v2[i];}/* プライマリー・スレッドは、このポイントの先に進む前に53.
target 領域の完了を待機します。*/
54.
55.
#pragma omp for
56.
for(i=n/2; i<n; i++) {57.
vxv[i] = v1[i]*v2[i];
58.
}
59.
/* ワークシェアリング for の終わりには暗黙の同期があります */
60.
}
61.
62.
end=omp_get_wtime();63.
64.
printf("vxv[0]=%f, vxv[n-1]=%f, time=%lf\n", vxv[0], vxv[n-1], end-start);65.
return 0;
66. }コンパイルコマンド$ icpx -fiopenmp -fopenmp-targets=spir64 test_target_no_nowait.cpp

実行コマンド$ OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0.0 LIBOMPTARGET_DEBUG=1 ./a.out

nowait 節を target 構造に追加すると、プライマリー・スレッドは target 領域の完了を待つことなく、for ループのワークシェリングを開始できるため、パフォーマンスが向上する可能性があります。for ループのワークシェリングの最後に暗黙のバリアがあるため、target 領域との同期が保証されます。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <time.h>
4. #include <omp.h>
5.
6. #define N 100000 // N は偶数
7.
8. void init(int n, float *v1, float *v2) {9.
int i;
10.
11.
for(i=0; i<n; i++){12.
v1[i] = i * 0.25;
13.
v2[i] = i - 1.25;
14.
}
15. }
16.
17. int main() {18.
int i, n=N;
19.
float v1[N],v2[N],vxv[N];20.
double start,end; // タイマー
21.
22.
init(n, v1,v2);
23.
24.
/* 起動のオーバーヘッドを計測しないように、ダミーの parallel および target (nowait) 領域を挿入
25.
*/
26.
#pragma omp parallel
27.
{
28.
#pragma omp master29.
#pragma omp target nowait
30.
{;}
31.
}

231

oneAPI GPU 最適化ガイド
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.

start=omp_get_wtime();#pragma omp parallel{#pragma omp master#pragma omp target teams distribute parallel for nowait \map(to: v1[0:n/2])\
map(to: v2[0:n/2])\
map(from: vxv[0:n/2])
for(i=0; i<n/2; i++){vxv[i] = v1[i]*v2[i];}#pragma omp for
for(i=n/2; i<n; i++) {vxv[i] = v1[i]*v2[i];}/* ワークシェアリング for の終わりには暗黙の同期があります。Target 領域は、この時点までに51.
完了することが保証されます */
52.
}
53.
54.
end=omp_get_wtime();55.
56.
printf("vxv[1]=%f, vxv[n-1]=%f, time=%lf\n", vxv[1], vxv[n-1], end-start);57.
return 0;58. }ラボマシンで実行した 2 つのバージョンのパフォーマンスは次のようになりました。
no nowait version: 0.008220 seconds
nowait on target version : 0.002110 seconds

Fortran
上記と同じ nowait の例を Fortran で記述すると次のようになります。
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.

232

!
! This test is from OpenMP API 5.0.1 Examples (June 2020)! https://www.openmp.org/wp-content/uploads/openmp-examples-5-0-1.pdf!(4.13.2 nowait Clause on target Construct)!
subroutine init(n, v1, v2)
integer :: i, nreal :: v1(n), v2(n)do i = 1, nv1(i) = i * 0.25
v2(i) = i - 1.25
end doend subroutine init
program test_target_nowait
use omp_lib
use iso_fortran_env
implicit none
integer, parameter :: NUM=100000 ! NUM は偶数でなければなりません
real :: v1(NUM), v2(NUM), vxv(NUM)
integer :: n, ireal(kind=REAL64) :: start, end
n = NUMcall init(n, v1, v2)

oneAPI GPU 最適化ガイド
29.
30.
入
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63. 11064.
65.

5-15-7-4! 起動のオーバーヘッドを計測しないように、ダミーの parallel および target (nowait) 領域を挿!
!$omp parallel!$omp master!$omp target nowait!$omp end target!$omp end master!$omp end parallel
start=omp_get_wtime()!$omp parallel!$omp master!$omp target teams distribute parallel do nowait &!$omp& map(to: v1(1:n/2)) &!$omp& map(to: v2(1:n/2)) &!$omp& map(from: vxv(1:n/2))
do i = 1, n/2
vxv(i) = v1(i)*v2(i)end do!$omp end master!$omp dodo i = n/2+1, nvxv(i) = v1(i)*v2(i)end do!$omp end parallel
end=omp_get_wtime()
write(*,110) "vxv(1)=", vxv(1), ", vxv(n-1)=", vxv(n-1), ", time=", end-start
format (A, F10.6, A, F17.6, A, F10.6)
end program test_target_nowait

メモリー割り当て

ここでは、いくつかのメモリー割り当て方法と、サポートされる割り当てタイプについて説明します。ホスト上のポイン
ターは、デバイス上のポインターと同じサイズです。
ホスト割り当ては、ホストが所有し、システムメモリーから割り当てることを意図しています。ホスト割り当ては、ホストとサポートされるすべてのデバイスでアクセスできます。そのため、ホスト割り当てのポインターは、ホストとサポー
トされるデバイスで同じポインターを使用できます。ホスト割り当てでは、システムメモリーとデバイスのローカルメ
モリー間での移行は想定されていません。デバイス上でホスト割り当てのポインターがアクセスされると、データは通常、デバイスとホストを接続する PCIe* などのバスを介して転送されます。
デバイス割り当ては、デバイスが所有し、デバイス・ローカル・メモリー外に割り当てることを意図しています。割り当てられたストレージは、該当するデバイスからリード/ライトできますが、ホストや他のサポートされるデバイスからは直接アクセスできません。
共有割り当ては、ホストとサポートされるすべてのデバイスでアクセスできます。そのため、ホスト割り当てのように、ホストとサポートされるすべてのデバイスは、共有割り当てメモリーを指す同じポインターを使用できます。ただし、共有割り当ては、特定のデバイスが所有するものではなく、ホストと複数のデバイス間での移行を意図するものです。移
行後のデバイスでのアクセスは、レイテンシーが大きいバスを介したシステムメモリーのアクセスではなく、高速なデバイス・ローカル・メモリーであることを意味します。
233

oneAPI GPU 最適化ガイド
共有システム割り当ては、共有割り当てのサブクラスであり、メモリーはシステム・アロケーター (malloc や new など) ではなく、割り当て API (OpenMP* のメモリー割り当て API) を使用します。共有システム割り当ては関連するデバイスを持たず、本質的にクロスデバイスです。ほかの共有割り当てと同様に、共有システム割り当ては、ホストとサポートされるデバイス間の移行を意図しているため、共有システム割り当てへの同じポインターがホストとすべてのデバイス間で使用される可能性があります。
注:•

現在、インテル® データセンター GPU マックス・シリーズのシステムでは共有システム割り当てはサポートされていません。ただし、割り当て API によって割り当てられる、共有割り当てはサポートされます。

次の表は、各タイプのメモリー割り当て特性をまとめたものです。
割り当てタイプ
ホスト
デバイス
共有
共有システム

初期の場所
ホスト
デバイス
ホスト、デバイス、または未指定
ホスト

ホストでアクセス可能
はい
いいえ
はい

デバイスでアクセス可能
はい
はい
はい

はい

はい

ホスト割り当ては広範囲なアクセス (ホストおよびサポートされるすべてのデバイスからの直接アクセス) を提供しま
すが、一般的にデータは PCIe* などのバスを介して転送されるためアクセスのコストは高くなります。
共有割り当ても幅広いアクセスを提供しますが、アクセスしたデバイスにデータが移行されるため、アクセスごとのコ
ストはホスト割り当てよりも低くなる可能性があります。
デバイス割り当てにはアクセス制限 (ホストや他のサポートされるデバイスから直接アクセスできない) がありますが、デバイスのローカルメモリーにアクセスできるため、高いパフォーマンスを発揮します。

5-15-7-4-1

メモリー割り当ての OpenMP* ランタイムルーチン

インテル® コンパイラーは、メモリー割り当てを行う多くの OpenMP* ランタイムルーチンをサポートしています。
それらのルーチンを次の表に示します。
OpenMP* メモリー割り当てルーチン
omp_target_alloc
omp_target_alloc_device
omp_target_alloc_host
omp_target_alloc_shared

インテル拡張
いいえ
はい
はい
はい

割り当てタイプ
デバイス
デバイス
ホスト
共有

omp_target_alloc_device、omp_target_alloc_host および omp_target_alloc_shared の 3 つのルーチンは、インテルによる OpenMP* 仕様の拡張であることに注意してください。
次の例では、上記の OpenMP* メモリー割り当てルーチンを使用しています。map 節を使用した場合と比較します。
メモリー割り当ての詳細については、以下を参照してください。
•
•
•
234

『Data Parallel C++』 James Reinders 共著 (英語)SYCL 2020 仕様 リビジョン 6 日本語版
oneAPI ゼロレベル仕様 (英語)oneAPI GPU 最適化ガイド•

このドキュメントの SYCL* セクション

5-15-7-4-2

map 節を使用する

最初の例では、map 節を使用してデバイス上にメモリーを割り当てて、ホストとデバイス間でデータをコピーしています。
次の例では、配列 A、B、C は、C/C++ の標準ライブラリー・ルーチン malloc によってシステムメモリーに割り当てられています。
52 行目の target 構造はデバイス上で配列 C の値を計算するカーネルです。この target 構造では、計算前にホストからデバイスへ、 そして計算後にデバイスからホストへ C の値を転送する必要があるため、 map(tofrom:C[0:length) 節が指定されています。配列 A と B の値は、ホストからデバイスに転送する必要があり、デバイスではこれらの値を読み取るだけなので map(to: A[0:length], B[0:length]) が指定されています。そして、map
節は、配列のストレージをデバイス上に割り当て、データをホストからデバイスへ、そしてデバイスからホストへコピーすることを指示しています。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <stdint.h>
4. #include <math.h>
5. #include <omp.h>
6.
7. #define iterations 100
8. #define length
64*1024*1024
9.
10. int main(void)
11. {
12.
size_t bytes = length*sizeof(double);13.
double * __restrict A;
14.
double * __restrict B;
15.
double * __restrict C;
16.
double scalar = 3.0;
17.
double nstream_time = 0.0;18.
19.
// malloc() を使用してホスト上に配列を割り当てます20.
21.
A = (double *) malloc(bytes);
22.
if (A == NULL){
23.
printf(" ERROR: Cannot allocate space for A using plain malloc().\n");24.
exit(1);
25.
}
26.
27.
B = (double *) malloc(bytes);
28.
if (B == NULL){
29.
printf(" ERROR: Cannot allocate space for B using plain malloc().\n");30.
exit(1);
31.
}
32.
33.
C = (double *) malloc(bytes);34.
if (C == NULL){
35.
printf(" ERROR: Cannot allocate space for C using plain malloc().\n");36.
exit(1);
37.
}
38.
39.
// 配列を初期化
40.
41.
#pragma omp parallel for42.
for (size_t i=0; i<length; i++) {43.
A[i] = 2.0;44.
B[i] = 2.0;

235

oneAPI GPU 最適化ガイド
45.
C[i] = 0.0;
46.
}
47.
48.
// 計算を実行
49.
50.
nstream_time = omp_get_wtime();51.
for (int iter = 0; iter<iterations; iter++) {52.
#pragma omp target teams distribute parallel for \53.
map(to: A[0:length], B[0:length]) \54.
map(tofrom: C[0:length])55.
for (size_t i=0; i<length; i++) {56.
C[i] += A[i] + scalar * B[i];
57.
}
58.
}
59.
nstream_time = omp_get_wtime() - nstream_time;
60.
61.
// 結果を評価して出力
62.
63.
double ar = 2.0;
64.
double br = 2.0;
65.
double cr = 0.0;66.
for (int iter = 0; iter<iterations; iter++) {67.
for (int i=0; i<length; i++) {68.
cr += ar + scalar * br;
69.
}
70.
}
71.
72.
double asum = 0.0;73.
#pragma omp parallel for reduction(+:asum)74.
for (size_t i=0; i<length; i++) {75.
asum += fabs(C[i]);
76.
}
77.
78.
free(A);
79.
free(B);80.
free(C);81.
82.
double epsilon=1.e-8;
83.
if (fabs(cr - asum)/asum > epsilon) {84.
printf("Failed Validation on output array\n"85.
"
Expected checksum: %lf\n"86.
"
Observed checksum: %lf\n"87.
"ERROR: solution did not validate\n", cr, asum);88.
return 1;
89.
} else {90.
printf("Solution validates\n");91.
double avgtime = nstream_time/iterations;92.
printf("Checksum = %lf; Avg time (s): %lf\n", asum, avgtime);
93.
}
94.
95.
return 0;
96. }コンパイルコマンド$ icpx -fiopenmp -fopenmp-targets=spir64 test_target_map.cpp

実行コマンド$ OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0.0 LIBOMPTARGET_DEBUG=1 ./a.out

236

oneAPI GPU 最適化ガイド
反復ループ内の target 構造の map 節は、それぞれの target 領域の入り口でデータ (A、B、C の値) をホストからデバイスに転送し、各 target 領域の出口でデータ (C の値) をデバイスからホストに転送します。これらのデータ転送には重大なパフォーマンス上のオーバーヘッドが発生します。map 節を使用する最善のアプローチは、反復ループ全体を map 節が関連する target data 構造に含むことです。これにより、転送は反復ループの入り口で一度、そして反復ループの出口で一度行われます。target data と map 節を使用した修正例を以下に示します。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <stdint.h>
4. #include <math.h>
5. #include <omp.h>
6.
7. #define iterations 100
8. #define length
64*1024*1024
9.
10. int main(void)
11. {
12.
size_t bytes = length*sizeof(double);13.
double * __restrict A;
14.
double * __restrict B;
15.
double * __restrict C;
16.
double scalar = 3.0;
17.
double nstream_time = 0.0;18.
19.
// malloc() を使用してホスト上に配列を割り当てます20.
21.
A = (double *) malloc(bytes);
22.
if (A == NULL){
23.
printf(" ERROR: Cannot allocate space for A using plain malloc().\n");24.
exit(1);
25.
}
26.
27.
B = (double *) malloc(bytes);
28.
if (B == NULL){
29.
printf(" ERROR: Cannot allocate space for B using plain malloc().\n");30.
exit(1);
31.
}
32.
33.
C = (double *) malloc(bytes);34.
if (C == NULL){
35.
printf(" ERROR: Cannot allocate space for C using plain malloc().\n");36.
exit(1);
37.
}
38.
39.
// 配列を初期化
40.
41.
#pragma omp parallel for42.
for (size_t i=0; i<length; i++) {43.
A[i] = 2.0;44.
B[i] = 2.0;45.
C[i] = 0.0;
46.
}
47.
48.
// 計算を実行
49.
50.
nstream_time = omp_get_wtime();51.
#pragma omp target data map(to: A[0:length], B[0:length]) \52.
map(tofrom: C[0:length])
53.
{
54.
for (int iter = 0; iter<iterations; iter++) {55.
#pragma omp target teams distribute parallel for56.
for (size_t i=0; i<length; i++) {57.
C[i] += A[i] + scalar * B[i];
58.
}
59.
}

237

oneAPI GPU 最適化ガイド
60.
}
61.
nstream_time = omp_get_wtime() - nstream_time;
62.
63.
// 結果を評価して出力
64.
65.
double ar = 2.0;
66.
double br = 2.0;
67.
double cr = 0.0;68.
for (int iter = 0; iter<iterations; iter++) {69.
for (int i=0; i<length; i++) {70.
cr += ar + scalar * br;
71.
}
72.
}
73.
74.
double asum = 0.0;75.
#pragma omp parallel for reduction(+:asum)76.
for (size_t i=0; i<length; i++) {77.
asum += fabs(C[i]);
78.
}
79.
80.
free(A);81.
free(B);82.
free(C);83.
84.
double epsilon=1.e-8;
85.
if (fabs(cr - asum)/asum > epsilon) {86.
printf("Failed Validation on output array\n"87.
"
Expected checksum: %lf\n"88.
"
Observed checksum: %lf\n"89.
"ERROR: solution did not validate\n", cr, asum);90.
return 1;
91.
} else {92.
printf("Solution validates\n");93.
double avgtime = nstream_time/iterations;94.
printf("Checksum = %lf; Avg time (s): %lf\n", asum, avgtime);
95.
}
96.
97.
return 0;
98. }5-15-7-4-3

omp_target_alloc

次に、上記の例を修正して map 節の代わりにデバイス割り当てを使用します。配列 A、B、および C のストレージは、
OpenMP* ランタイムルーチン omp_target_alloc を呼び出してデバイス上に直接割り当てられます。このルーチ
ンは、デバイス上に割り当てるバイト数と、ストレージを割り当てるデバイス番号の 2 つの引数を受け取り、デバイス上に割り当てられたストレージのデバイスアドレスを参照するデバイスポインターを返します。omp_target_alloc
が NULL を返す場合、割り当てが成功しなかったことを意味します。
target 構造に割り当てられたメモリーにアクセスするには、omp_target_alloc を呼び出して取得したデバイ
スポインターを target 構造の is_device_ptr 節に指定します。これにより、カーネルはデバイス上に存在するデータを操作するため、カーネル実行の前後でデータ転送が発生しないようになります。
プログラムの最後でランタイムルーチン omp_target_free を呼び出して、A、B、C のストレージ割り当てを解放し
ます。
1.
2.
3.
4.
5.
6.
7.

238

#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <math.h>#include <omp.h>#define iterations 100

oneAPI GPU 最適化ガイド
8. #define length
64*1024*1024
9.
10. int main(void)
11. {
12.
int device_id = omp_get_default_device();13.
size_t bytes = length*sizeof(double);
14.
double * __restrict A;
15.
double * __restrict B;
16.
double * __restrict C;
17.
double scalar = 3.0;
18.
double nstream_time = 0.0;
19.
20.
// デバイスメモリーに配列を割り当て
21.
22.
A = (double *) omp_target_alloc(bytes, device_id);23.
if (A == NULL){
24.
printf(" ERROR: Cannot allocate space for A using omp_target_alloc().\n");25.
exit(1);
26.
}
27.
28.
B = (double *) omp_target_alloc(bytes, device_id);29.
if (B == NULL){
30.
printf(" ERROR: Cannot allocate space for B using omp_target_alloc().\n");31.
exit(1);
32.
}
33.
34.
C = (double *) omp_target_alloc(bytes, device_id);35.
if (C == NULL){
36.
printf(" ERROR: Cannot allocate space for C using omp_target_alloc().\n");37.
exit(1);
38.
}
39.
40.
// 配列を初期化
41.
42.
#pragma omp target teams distribute parallel for \43.
is_device_ptr(A,B,C)
44.
for (size_t i=0; i<length; i++) {45.
A[i] = 2.0;
46.
B[i] = 2.0;47.
C[i] = 0.0;
48.
}
49.
50.
// 計算を iterations 回繰り返します51.
52.
nstream_time = omp_get_wtime();53.
for (int iter = 0; iter<iterations; iter++) {54.
#pragma omp target teams distribute parallel for \55.
is_device_ptr(A,B,C)
56.
for (size_t i=0; i<length; i++) {57.
C[i] += A[i] + scalar * B[i];
58.
}
59.
}
60.
nstream_time = omp_get_wtime() - nstream_time;
61.
62.
// 結果を評価して出力
63.
64.
double ar = 2.0;
65.
double br = 2.0;
66.
double cr = 0.0;67.
for (int iter = 0; iter<iterations; iter++) {68.
for (int i=0; i<length; i++) {69.
cr += ar + scalar * br;
70.
}
71.
}
72.
73.
double asum = 0.0;74.
#pragma omp target teams distribute parallel for reduction(+:asum) \239

oneAPI GPU 最適化ガイド
75.
map(tofrom: asum) is_device_ptr(C)76.
for (size_t i=0; i<length; i++) {77.
asum += fabs(C[i]);
78.
}
79.
80.
omp_target_free(A, device_id);81.
omp_target_free(B, device_id);82.
omp_target_free(C, device_id);83.
84.
double epsilon=1.e-8;
85.
if (fabs(cr - asum)/asum > epsilon) {86.
printf("Failed Validation on output array\n"87.
"
Expected checksum: %lf\n"88.
"
Observed checksum: %lf\n"89.
"ERROR: solution did not validate\n", cr, asum);90.
return 1;
91.
} else {92.
printf("Solution validates\n");93.
double avgtime = nstream_time/iterations;94.
printf("Checksum = %lf; Avg time (s): %lf\n", asum, avgtime);
95.
}
96.
97.
return 0;
98. }

注:
•
•
•

omp_target_alloc を呼び出す場合、指定するデバイス番号はホストデバイス以外のサポートされるデバイスでなければなりません。これがストレージを割り当てるデバイスとなります。
配列 A、B、C はホストからアクセスできないため、配列の初期化、カーネル実行、および C の要素の合計はす
べて OpenMP* target 領域内で行う必要があります。
デバイス割り当ては omp_target_alloc 呼び出しで指定されたデバイスからのみアクセスできますが、
omp_target_memcpy を呼び出してホストまたは他のデバイスに割り当てられたメモリーにコピーできます。

5-15-7-4-4

omp_target_alloc_device

インテル拡張機能である omp_target_alloc_device は、omp_target_alloc に類似しており、デバイス上に割り当てるバイト数と、ストレージを割り当てるデバイス番号の 2 つの引数を受け取ります。デバイス上に割り当てられたストレージのデバイスアドレスを参照するデバイスポインターを返します。omp_target_alloc_device が
NULL を返す場合、割り当てが成功しなかったことを意味します。
上記の omp_target_alloc の例は、次のように omp_target_alloc 呼び出しを omp_target_alloc 呼び出
しに置き換えるだけで書き換えできます。
プログラムの最後でランタイムルーチン omp_target_free を呼び出して、A、B、C のストレージ割り当てを解放します。
// デバイスメモリーに配列を割り当て
A = (double *) omp_target_alloc_device(bytes, device_id);
if (A == NULL){
printf(" ERROR: Cannot allocate space for A using omp_target_alloc_device().\n");exit(1);}B = (double *) omp_target_alloc_device(bytes, device_id);
if (B == NULL){
printf(" ERROR: Cannot allocate space for B using omp_target_alloc_device().\n");exit(1);240

oneAPI GPU 最適化ガイド}
C = (double *) omp_target_alloc_device(bytes, device_id);
if (C == NULL){
printf(" ERROR: Cannot allocate space for C using omp_target_alloc_device().\n");exit(1);}

注:•

omp_target_alloc に適用される注意事項は、omp_target_alloc_device にも同様に適用されます。

5-15-7-4-5

omp_target_alloc_host

上記の例は、A、B、C をホストに割り当てるように書き換えることもできます。これにより、ホストとサポートされるすべてのデバイスからメモリーにアクセスできるようになります。
以下の修正例では、omp_target_alloc_host ランタイムルーチン (インテル拡張機能) を呼び出して、配列 A、B、および C にストレージを割り当てています。このルーチンは、割り当てバイト数とデバイス番号を引数として受け取ります。デバイス番号はホストデバイス以外のサポートされるデバイスでなければなりません。このルーチンは、ホスト
メモリーの格納場所へのポインターを返します。omp_target_alloc_host が NULL を返す場合、割り当てが成功しなかったことを意味します。
プログラムの最初に unified_address ディレクティブが指定されていることに注意してください。これには、OpenMP* API ルーチンとディレクティブを介してアクセス可能なすべてのデバイスが、統合アドレス空間を使用できる実装を保証する必要があります。このアドレス空間では、ポインターは常にすべてのデバイスで同じメモリー位置を参照します。ターゲット領域内で使用するためデバイスポインターからデバイスアドレスを取得する
is_device_ptr 節は必要ありません。インテル® コンパイラーを使用するとデフォルトで統合アドレス空間が保障されているため、unified_address ディレクティブは必要ありませんが、移植性のためディレクティブを残しておくと良いでしょう。
omp_target_alloc_host 呼び出しが返すポインターを使用して、ホストとサポートされるすべてのデバイスから
ストレージにアクセスできます。統合アドレス空間が使用されているので、デバイスからメモリーにアクセスするため、target 構造に map 節や is_device_ptr 節は必要ありません。
プログラムの最後でランタイムルーチン omp_target_free を呼び出して、A、B、C のストレージ割り当てを解放
します。

241

oneAPI GPU 最適化ガイド
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <stdint.h>
4. #include <math.h>
5. #include <omp.h>
6.
7. #pragma omp requires unified_address
8.
9. #define iterations 100
10. #define length
64*1024*1024
11.
12. int main(void)
13. {
14.
int device_id = omp_get_default_device();15.
size_t bytes = length*sizeof(double);
16.
double * __restrict A;
17.
double * __restrict B;
18.
double * __restrict C;
19.
double scalar = 3.0;
20.
double nstream_time = 0.0;
21.
22.
// ホストメモリーに配列を割り当て
23.
24.
A = (double *) omp_target_alloc_host(bytes, device_id);25.
if (A == NULL){
26.
printf(" ERROR: Cannot allocate space for A using
omp_target_alloc_host().\n");27.
exit(1);
28.
}
29.
30.
B = (double *) omp_target_alloc_host(bytes, device_id);31.
if (B == NULL){
32.
printf(" ERROR: Cannot allocate space for B using
omp_target_alloc_host().\n");33.
exit(1);
34.
}
35.
36.
C = (double *) omp_target_alloc_host(bytes, device_id);37.
if (C == NULL){
38.
printf(" ERROR: Cannot allocate space for C using
omp_target_alloc_host().\n");39.
exit(1);
40.
}
41.
42.
// 配列を初期化
43.
44.
#pragma omp parallel for45.
for (size_t i=0; i<length; i++) {46.
A[i] = 2.0;47.
B[i] = 2.0;
48.
C[i] = 0.0;
49.
}
50.
51.
// 計算を実行
52.
53.
nstream_time = omp_get_wtime();54.
for (int iter = 0; iter<iterations; iter++) {55.
#pragma omp target teams distribute parallel for56.
for (size_t i=0; i<length; i++) {57.
C[i] += A[i] + scalar * B[i];
58.
}
59.
}
60.
nstream_time = omp_get_wtime() - nstream_time;
61.
62.
// 結果を評価して出力
63.
64.
double ar = 2.0;242

oneAPI GPU 最適化ガイド
65.
double br = 2.0;
66.
double cr = 0.0;67.
for (int iter = 0; iter<iterations; iter++) {68.
for (int i=0; i<length; i++) {69.
cr += ar + scalar * br;
70.
}
71.
}
72.
73.
double asum = 0.0;74.
#pragma omp parallel for reduction(+:asum)75.
for (size_t i=0; i<length; i++) {76.
asum += fabs(C[i]);
77.
}
78.
79.
omp_target_free(A, device_id);80.
omp_target_free(B, device_id);81.
omp_target_free(C, device_id);82.
83.
double epsilon=1.e-8;
84.
if (fabs(cr - asum)/asum > epsilon) {85.
printf("Failed Validation on output array\n"86.
"
Expected checksum: %lf\n"87.
"
Observed checksum: %lf\n"88.
"ERROR: solution did not validate\n", cr, asum);89.
return 1;
90.
} else {91.
printf("Solution validates\n");92.
double avgtime = nstream_time/iterations;93.
printf("Checksum = %lf; Avg time (s): %lf\n", asum, avgtime);
94.
}
95.
96.
return 0;
97. }

注:
•
•
•

omp_target_alloc_host を呼び出す場合、指定するデバイス番号はホストデバイス以外のサポートされるデバイスでなければなりません。
配列 A、B および C はホストとデバイスからアクセスできるため、配列の初期化と C の要素の合計は、ホスト(target 構造の外で) またはデバイス (target 構造の中) のどちらでも実行できます。
インテル® データセンター GPU マックス・シリーズは、ホスト割り当て (omp_target_alloc_host で割り当
てられたメモリー) のアトミック操作 (または、一部のリダクションなどアトミック操作を行うアルゴリズム) をサ
ポートしていません。代わりに、omp_target_alloc_device で割り当てたメモリーでアトミック操作を行います。

5-15-7-4-6

omp_target_alloc_shared

上記の例では、ホスト割り当ての代わりに共有割り当てを使用するように変更されています。
omp_target_alloc_shared ランタイムルーチンは、配列 A、B、および C にストレージを割り当てています。このルーチンは、割り当てバイト数とデバイス番号を引数として受け取ります。デバイス番号はホストデバイス以外のサポートされるデバイスでなければなりません。このルーチンは、共有メモリーの格納場所へのポインターを返します。
omp_target_alloc_shared が NULL を返すと、割り当てが成功しなかったことを意味します。
移植性のため、プログラムの最初に unified_address ディレクティブが指定されていることに注意してください。
omp_target_alloc_shared 呼び出しが返すポインターを使用して、ホストとサポートされるすべてのデバイスからストレージにアクセスできます。統合アドレス空間が使用されているので、デバイスからメモリーをアクセスするため、target 構造に map 節や is_device_ptr 節は必要ありません。

243

oneAPI GPU 最適化ガイド
プログラムの最後でランタイムルーチン omp_target_free を呼び出して、A、B、C のストレージ割り当てを解放します。
// 共有メモリーに配列を割り当て
A = (double *) omp_target_alloc_shared(bytes, device_id);
if (A == NULL){
printf(" ERROR: Cannot allocate space for A using omp_target_alloc_shared().\n");exit(1);}B = (double *) omp_target_alloc_shared(bytes, device_id);
if (B == NULL){
printf(" ERROR: Cannot allocate space for B using omp_target_alloc_shared().\n");exit(1);}C = (double *) omp_target_alloc_shared(bytes, device_id);
if (C == NULL){
printf(" ERROR: Cannot allocate space for C using omp_target_alloc_shared().\n");exit(1);}

注:•

omp_target_alloc_shared を呼び出す場合、指定するデバイス番号はホストデバイス以外のサポートされるデバイスでなければなりません。
配列はホストとデバイスからアクセスできるため、初期化と検証はホストまたはデバイス (target 構造内) のど
ちらでも実行できます。
omp_target_alloc_shared で割り当てたメモリーへのホストとデバイスからの同時アクセスはサポートされていません。

••

5-15-7-4-7

omp_target_memcpy

次の例は、ランタイムルーチン omp_target_memcpy を使用して、ホストからデバイスへ、またはデバイスからホス
トへメモリーをコピーする方法を示しています。最初の配列 h_A、h_B および h_C は、通常の malloc でシステムメ
モリーに割り当てられてから初期化されています。対応する配列 d_A、d_B および d_C は、omp_target_alloc に
よってデバイスに割り当てられています。
98 行目で、target 構造を開始する前に、h_A、h_B、そして h_C の値が omp_target_memcpy を呼び出して d_A、
d_B、d_C にコピーされます。target 領域の後で、omp_target_memcpy を呼び出してデバイスで計算された新しい d_C の値が h_C にコピーされます。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <stdint.h>
4. #include <math.h>
5. #include <omp.h>
6.
7. #define iterations 100
8. #define length
64*1024*1024
9.
10. int main(void)
11. {
12.
int device_id = omp_get_default_device();13.
int host_id = omp_get_initial_device();14.
size_t bytes = length*sizeof(double);15.
double * __restrict h_A;
16.
double * __restrict h_B;

244

oneAPI GPU 最適化ガイド
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.

double * __restrict h_C;
double * __restrict d_A;
double * __restrict d_B;
double * __restrict d_C;
double scalar = 3.0;
double nstream_time = 0.0;// 通常の malloc() を使用して、配列 h_A、h_B、および h_C をホストに割り当てます
h_A = (double *) malloc(bytes);
if (h_A == NULL){
printf(" ERROR: Cannot allocate space for h_A using plain malloc().\n");exit(1);}h_B = (double *) malloc(bytes);
if (h_B == NULL){
printf(" ERROR: Cannot allocate space for h_B using plain malloc().\n");exit(1);}
h_C = (double *) malloc(bytes);
if (h_C == NULL){
printf(" ERROR: Cannot allocate space for h_C using plain malloc().\n");exit(1);}// omp_target_alloc() を使用して、配列 d_A、d_B、および d_C をデバイスに割り当てます
d_A = (double *) omp_target_alloc(bytes, device_id);
if (d_A == NULL){
printf(" ERROR: Cannot allocate space for d_A using omp_target_alloc().\n");exit(1);}d_B = (double *) omp_target_alloc(bytes, device_id);
if (d_B == NULL){
printf(" ERROR: Cannot allocate space for d_B using omp_target_alloc().\n");exit(1);}
d_C = (double *) omp_target_alloc(bytes, device_id);
if (d_C == NULL){
printf(" ERROR: Cannot allocate space for d_C using omp_target_alloc().\n");exit(1);}// ホスト上で配列を初期化します#pragma omp parallel forfor (size_t i=0; i<length; i++) {h_A[i] = 2.0;
h_B[i] = 2.0;
h_C[i] = 0.0;}// omp_target_memcpy() を呼び出して、ホストからデバイスへ値をコピーしますint rc = 0;rc = omp_target_memcpy(d_A, h_A, bytes, 0, 0, device_id, host_id);
if (rc) {printf("ERROR: omp_target_memcpy(A) returned %d\n", rc);exit(1);}
rc = omp_target_memcpy(d_B, h_B, bytes, 0, 0, device_id, host_id);
if (rc) {245

oneAPI GPU 最適化ガイド
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.

246

printf("ERROR: omp_target_memcpy(B) returned %d\n", rc);exit(1);}
rc = omp_target_memcpy(d_C, h_C, bytes, 0, 0, device_id, host_id);
if (rc) {printf("ERROR: omp_target_memcpy(C) returned %d\n", rc);exit(1);}// 計算を実行
nstream_time = omp_get_wtime();for (int iter = 0; iter<iterations; iter++) {#pragma omp target teams distribute parallel for \is_device_ptr(d_A,d_B,d_C)
for (size_t i=0; i<length; i++) {d_C[i] += d_A[i] + scalar * d_B[i];}}nstream_time = omp_get_wtime() - nstream_time;// omp_target_memcpy() を呼び出して、デバイスからホストへ値をコピーしますrc = omp_target_memcpy(h_C, d_C, bytes, 0, 0, host_id, device_id);
if (rc) {printf("ERROR: omp_target_memcpy(A) returned %d\n", rc);exit(1);}// 結果を評価して出力
double ar = 2.0;
double br = 2.0;
double cr = 0.0;
for (int iter = 0; iter<iterations; iter++) {for (int i=0; i<length; i++) {cr += ar + scalar * br;}}
double asum = 0.0;
#pragma omp parallel for reduction(+:asum)for (size_t i=0; i<length; i++) {asum += fabs(h_C[i]);}free(h_A);
free(h_B);
free(h_C);
omp_target_free(d_A, device_id);
omp_target_free(d_B, device_id);
omp_target_free(d_C, device_id);double epsilon=1.e-8;
if (fabs(cr - asum)/asum > epsilon) {printf("Failed Validation on output array\n""Expected checksum: %lf\n""Observed checksum: %lf\n""ERROR: solution did not validate\n", cr, asum);
return 1;} else {printf("Solution validates\n");double avgtime = nstream_time/iterations;printf("Checksum = %lf; Avg time (s): %lf\n", asum, avgtime);}
return 0;

oneAPI GPU 最適化ガイド
152. }

5-15-7-4-8

パフォーマンス上の考察

上 記 の 例 (map 節 、 omp_target_alloc 、 omp_target_alloc_device 、 omp_target_alloc_host 、
omp_target_alloc_shared、omp_target_memcpy を使用) では、メインのカーネルは配列 C の値を計算する
target 構造です。正確なタイミングを取得するため、この target 構造はループ内にありオフロードを反復します(反復数は 100)。反復ループにかかった合計時間を 100 で割って、平均カーネル時間を計算します。
// 計算を iterations 回繰り返します
nstream_time = omp_get_wtime();for (int iter = 0; iter<iterations; iter++) {#pragma omp target teams distribute parallel for \is_device_ptr(A,B,C)
for (size_t i=0; i<length; i++) {C[i] += A[i] + scalar * B[i];}}nstream_time = omp_get_wtime() - nstream_time;

LIBOMPTARGET_DEBUG=1 の出力は、上記のすべての例が同じ ND_range のパーティション化であることを示しています。
Target LEVEL0 RTL --> Allocated a device memory 0xff00000020200000
Libomptarget --> omp_target_alloc returns device ptr 0xff00000020200000
Libomptarget --> Call to omp_target_alloc for device 0 requesting 536870912 bytes
Libomptarget --> Call to omp_get_num_devices returning 1
Libomptarget --> Call to omp_get_initial_device returning 1
Libomptarget --> Checking whether device 0 is ready.
Libomptarget --> Is the device 0 (local ID 0) initialized? 1
Libomptarget --> Device 0 is ready to use.

次の表は、使用した特定の GPU (1 スタックのみ) で実行した際の、それぞれのカーネルが要した平均時間です。
バ―ション
map
map + target data
omp_target_alloc
omp_target_alloc_device
omp_target_alloc_host
omp_target_alloc_shared
omp_target_memcpy

時間 (秒)
0.183604
0.012757
0.002501
0.002499
0.074412
0.012491
0.011072

上記のパフォーマンス値を見ると、map バージョンが最も低速である (0.183604 秒) ことが分かります。これは、各カーネルの起動、および最初と最後のデータ転送が影響しています。メインのカーネルは 100 回起動されています。
各カーネル起動の最初でデバイス上に配列 A、B、C のストレージが確保され、これらの配列の値がホストからデバイスにコピーされます。カーネルの最後で、配列 C の値はデバイスからホストにコピーされます。反復ループ全体を map
節を持つ target data 構造内に置くと、反復ループの最初のカーネル起動に 1 回、そしてループの最後のカーネルの後に 1 回転送が行われるため、実行時間は 0.012757 秒に短縮されます。

247

oneAPI GPU 最適化ガイド
omp_target_alloc と omp_target_alloc_device のバージョンが最良のパフォーマンスを示しています(0.002501 秒と 0.002499 秒)。これらのバージョンでは、A、B および C のストレージはデバイスメモリーに直接割り当てられ、デバイス上のアクセスはデバイスのローカルメモリーになります。これは、デバイス側でスクラッチの配列を使用するアプリケーションには有用なモデルです。これらの配列は、ホストでアクセスする必要がありません。このような場合、例に示すように、デバイスに配列を割り当てて、データを転送しないことを推奨します。
omp_target_alloc_shared バージョンの良好なパフォーマンスを示していますが、最良の結果にはおよびません (0.012491 秒)。このバージョンでは、A、B および C のストレージは共有メモリーに割り当てられます。そのため、ホストとデバイス間でデータを移行することができます。移行によるオーバーヘッドはありますが、移行後はデバイス上のアクセスは高速なデバイスのローカルメモリーから行われます。このバージョンでは、配列の初期化はホストで行われます。最初のカーネル起動時に配列がデバイスに移行され、カーネルはデバイス上でローカルに配列にアクセスします。最後に、ホストがリダクション計算を行う前に、C 配列全体がホストに移行されます。
omp_target_alloc_host バージョン (0.074412 秒) は、omp_target_alloc_shared バージョンよりもおよ
そ 6 倍の時間を要しています。これは、ホストメモリーに割り当てられたデータが、ホストからデバイスに移行しないためです。データは通常、デバイスとホストを接続する PCIe* などのバスを介して転送されます。これは、ローカルデバ
イスのメモリーアクセスよりも低速です。デバイス上でアクセスされるデータが少量であれば、
omp_target_alloc_host を使用してホストメモリーにデータを割り当てるべきです。ただし、配列がデバイス上で頻繁にアクセスされる場合、配列はデバイスメモリーに保持する必要があります。データをホストメモリーに保持してPCIe* 経由でアクセスすると、パフォーマンスが低下します。
最後に、データの転送に関する注意点を述べます。map バージョンの転送データ量は、LIBOMPTARGET_DEBUG=1 の出力に grep "Libomptarget --> Moving" を実行して確認できます。メインのカーネルを呼び出すごとに、次のようなデータ転送が行われることに注意してください。
$ grep "Libomptarget --> Moving" test_target_map.debug
Libomptarget --> Moving 536870912 bytes (hst:0x00007f1a5fc8b010) ->(tgt:0xff00000000200000)
Libomptarget --> Moving 536870912 bytes (hst:0x00007f1a9fc8d010) ->(tgt:0xff00000020200000)
Libomptarget --> Moving 536870912 bytes (hst:0x00007f1a7fc8c010) ->(tgt:0xff00000040200000)
Libomptarget --> Moving 536870912 bytes (tgt:0xff00000000200000) ->(hst:0x00007f1a5fc8b010)

一 方 、 omp_target_alloc_... バ ー ジ ョ ン の デ ー タ 転 送 は 、 ラ ン タ イ ム の 下 層 で 処 理 さ れ ま す 。
LIBOMPTARGET_DEBUG=1 の出力に grep "Libomptarget --> Moving" コマンドを実行してもデータ転送は表示されません。
Fortran の例
Fortran では target data と map 節を次のように使用します。
1. program main2.
use iso_fortran_env
3.
use omp_lib
4.
implicit none
5.
6.
integer, parameter :: iterations=100
7.
integer, parameter :: length=64*1024*1024
8.
real(kind=REAL64), parameter :: epsilon=1.D-8
9.
real(kind=REAL64), allocatable :: A(:)
10.
real(kind=REAL64), allocatable :: B(:)
11.
real(kind=REAL64), allocatable :: C(:)
12.
real(kind=REAL64) :: scalar=3.0
13.
real(kind=REAL64) :: ar, br, cr, asum248

oneAPI GPU 最適化ガイド
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.

real(kind=REAL64) :: nstream_time, avgtime
integer :: err, i, iter!
! ホスト上でプレーンな割り当てにより配列を割り当て
allocate( A(length), stat=err )if (err .ne. 0) thenprint *, "Allocation of A returned ", errstop 1endif
allocate( B(length), stat=err )if (err .ne. 0) thenprint *, "Allocation of B returned ", errstop 1endif
allocate( C(length), stat=err )if (err .ne. 0) thenprint *, "Allocation of C returned ", errstop 1endif!
! 配列の初期化!$omp parallel dodo i = 1, length
A(i) = 2.0
B(i) = 2.0
C(i) = 0.0
end do!
! 計算を実行nstream_time = omp_get_wtime()!$omp target data map(to: A, B) map(tofrom: C)
do iter = 1, iterations!$omp target teams distribute parallel dodo i = 1, length
C(i) = C(i) + A(i) + scalar * B(i)end doend do!$omp end target data
nstream_time = omp_get_wtime() - nstream_time!
! 結果を評価して出力
ar = 2.0
br = 2.0
cr = 0.0
do iter = 1, iterations
do i = 1, length
cr = cr + ar + scalar * brend doend doasum = 0.0!$omp parallel do reduction(+:asum)
do i = 1, length
asum = asum + abs(C(i))
end do249

oneAPI GPU 最適化ガイド
81.
82.
83.

if (abs(cr - asum)/asum > epsilon) thenwrite(*,110) "Failed Validation on output array: Expected =", cr, ",Observed =", asum
84.
else
85.
avgtime = nstream_time/iterations
86.
write(*,120) "Solution validates: Checksum =", asum, ", Avg time (s) =",avgtime
87.
endif
88.
89. 110 format (A, F20.6, A, F20.6)
90. 120 format (A, F20.6, A, F10.6)
91.
92.
deallocate(A)93.
deallocate(B)94.
deallocate(C)95.
96.
end program main

以 下 に 、 omp_target_alloc_device を 使 用 し た Fortran の 例 を 示 し ま す 。 こ の 例 で は 、 ア ロ ケ ー タ ー
omp_target_device_mem_alloc を使用して、デバイス上に配列 A、B そして C を割り当てています。target
data ディレクティブに use_device_addr(A, B, C) 節を指定して、配列がデバイスアドレスを持ち、アドレスがtarget 領域で使用されることを明示します。
1. use iso_fortran_env
2.
use omp_lib
3.
implicit none
4.
5.
integer, parameter :: iterations=100
6.
integer, parameter :: length=64*1024*1024
7.
real(kind=REAL64), parameter :: epsilon=1.D-8
8.
real(kind=REAL64), allocatable :: A(:)
9.
real(kind=REAL64), allocatable :: B(:)
10.
real(kind=REAL64), allocatable :: C(:)
11.
real(kind=REAL64) :: scalar=3.0
12.
real(kind=REAL64) :: ar, br, cr, asum13.
real(kind=REAL64) :: nstream_time, avgtime
14.
integer :: i, iter
15.
16.
!
17.
! デバイスメモリーに配列を割り当て
18.
19.
!$omp allocate (allocator(omp_target_device_mem_alloc): A)20.
allocate(A(length))21.
22.
!$omp allocate (allocator(omp_target_device_mem_alloc): B)
23.
allocate(B(length))24.
25.
!$omp allocate (allocator(omp_target_device_mem_alloc): C)
26.
allocate(C(length))
27.
28.
!
29.
! target data の始まり
30.
31.
!$omp target data use_device_addr(A, B, C)
32.
33.
!
34.
! 配列の初期化
35.
36.
!$omp target teams distribute parallel do37.
do i = 1, length38.
A(i) = 2.0
39.
B(i) = 2.0
40.
C(i) = 0.0
41.
end do250

oneAPI GPU 最適化ガイド
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.

!
! 計算を実行nstream_time = omp_get_wtime()
do iter = 1, iterations!$omp target teams distribute parallel dodo i = 1, length
C(i) = C(i) + A(i) + scalar * B(i)end doend donstream_time = omp_get_wtime() - nstream_time!
! 結果を評価して出力
ar = 2.0
br = 2.0
cr = 0.0
do iter = 1, iterations
do i = 1, length
cr = cr + ar + scalar * brend doend doasum = 0.0!$omp target teams distribute parallel do reduction(+:asum) &!$omp map(tofrom: asum)
do i = 1, length
asum = asum + abs(C(i))
end do!
! target data の終わり!$omp end target data

if (abs(cr - asum)/asum > epsilon) thenwrite(*,110) "Failed Validation on output array: Expected =", cr, ",Observed =", asum
81.
else
82.
avgtime = nstream_time/iterations
83.
write(*,120) "Solution validates: Checksum =", asum, ", Avg time (s) =",avgtime
84.
endif
85.
86. 110 format (A, F20.6, A, F20.6)
87. 120 format (A, F20.6, A, F10.6)
88.
89.
deallocate(A)90.
deallocate(B)91.
deallocate(C)92.
93.
end program main

5-15-7-5

Fortran の例

メモリー割り当てを適切に選択することで、冗長なデータ転送を回避してパフォーマンスを向上する Fortran の例を説明します。
この Fortran の例では、OpenMP* オフロード機能と、ハイパフォーマンス・アプリケーションである NWChem を使用しています。

251

oneAPI GPU 最適化ガイド
この例では、4 レベルの入れ子になったループがあります。最も内側の k ループは、sgemm への 2 つの呼び出しをデバイスにオフロードし、リダクション計算 (リダクション変数 emp4i と emp5i) をデバイスにオフロードする
omp_fbody ルーチンを呼び出します。
ループの入れ子では、いくつかの配列がホストで更新されます (配列 Tkj、Kkj、Jia、Kia、Tia、Xia および t1v1)。
そのため、デバイス上のこれらの配列の値とホストの値を一致させる必要があります。

5-15-7-5-1

バージョン 1

プログラムの最初のバージョンでは、allocate ディレクティブを使用して共有メモリーに 11 個の配列を割り当てています。
!$omp allocate (allocator(omp_target_shared_mem_alloc): f1n)
allocate( f1n(1:nvir,1:nvir) )!$omp allocate (allocator(omp_target_shared_mem_alloc): f2n)
allocate( f2n(1:nvir,1:nvir) )!$omp allocate (allocator(omp_target_shared_mem_alloc): eorb)
allocate( eorb(1:nbf) )...

共有割り当ては、ホストとデバイスからアクセスでき、必要に応じてホストとデバイス間で自動的に移行されます。そのため、共有割り当てへのポインターは、ホストとデバイスで同じポインターを使用できます。
バージョン 1 を以下に示します。
1. include "mkl_omp_offload.f90"
2.
3.
subroutine init (a, b, c, m, k, n)
4.
implicit none
5.
6.
real :: a(m, k), b(k,n), c(m,n)
7.
integer m, k, n, i, j
8.
9.
do i = 1, m
10.
do j = 1, k
11.
a(i, j) = i
12.
end do13.
end do14.
15.
do i = 1, k
16.
do j = 1, n
17.
b(i, j) = j - 1
18.
end do19.
end do20.
21.
do i = 1, m
22.
do j = 1, n23.
c(i, j) = 0.2 + i - j
24.
end do25.
end do26.
end subroutine init
27.
28.
29.
program main30.
31. #if defined(MKL_ILP64)
32.
use onemkl_blas_omp_offload_ilp64
33. #else34.
use onemkl_blas_omp_offload_lp64
35. #endif

252

oneAPI GPU 最適化ガイド
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95. 120
96. 130
97.
98.
99.
100.
101.
102.
103.

use omp_lib
use iso_fortran_env
implicit none
integer, parameter
integer, parameter
integer, parameter
integer, parameter
real, allocatablereal
integer
double precision:: m = 1024:: k = 1024:: n = 1024:: iter = 2000:: a(:, :), b(:, :), c(:, :):: alpha, beta, sum, total:: i, j1, j2:: t0, t1!$omp allocators allocate(allocator(omp_target_shared_mem_alloc): a)
allocate( a(1 : m, 1 : k) )!$omp allocators allocate(allocator(omp_target_shared_mem_alloc): b)
allocate( b(1 : k, 1 : n) )!$omp allocators allocate(allocator(omp_target_shared_mem_alloc): c)
allocate( c(1 : m, 1 : n) )! Initialize.
alpha = 1.025
beta = 1.0
total = 0.0
call init (a, b, c, m, k, n)! Compute sgemm on the device.
t0 = omp_get_wtime()
do i = 1, iter
! Update arrays a and b.
a(:,:) = a(:,:) + 1b(:,:) = b(:,:) - 1! Compute sgemm on the device.
!$omp dispatch
call sgemm('n','n',m,n,k,alpha,a,m,b,k,beta,c,m)
sum = 0.0!$omp target teams distribute parallel do collapse(2) reduction(+:sum)
do j1 = 1, m
do j2 = 1, n
sum = sum + c(j1,j2)
enddo
enddo
!$omp end target teams distribute parallel dototal = total + sum
end dot1 = omp_get_wtime()
print *, "total = ", total
write (*, 120) " Number of iterations = ", iterwrite (*, 130) " Time = ", t1-t0, " seconds"
format (A, I4)
format (A, F10.3, A)! Deallocate arrays.
deallocate(a)deallocate(b)deallocate(c)end program main

253

oneAPI GPU 最適化ガイド
このバージョンは最も簡単なプログラム構成ですが、効率も良くありません。

5-15-7-5-2

バージョン 2

プログラムのバージョン 2 では、単純な allocate を使用してシステムメモリーに行列を割り当て、map 句を使用して行列をデバイスにマップします。
allocate( a(1 : m, 1 : k) )allocate( b(1 : k, 1 : n) )allocate( c(1 : m, 1 : n) )...
$omp target data map(tofrom: a, b, c)

行列 a と b はホスト上で更新されるため、OpenMP* の target update to ディレクティブを使用して、行列の新しい値をホストからデバイスにコピーします。
a(:,:) = a(:,:) + 1b(:,:) = b(:,:) - 1! Copy new values of a and b to the device.
!$omp target update to (a, b)

完全なバージョン 2 を以下に示します。
1.
include "mkl_omp_offload.f90"
2.
3.
subroutine init (a, b, c, m, k, n)
4.
implicit none
5.
6.
real :: a(m, k), b(k,n), c(m,n)
7.
integer m, k, n, i, j
8.
9.
do i = 1, m
10.
do j = 1, k
11.
a(i, j) = i
12.
end do13.
end do14.
15.
do i = 1, k
16.
do j = 1, n
17.
b(i, j) = j - 1
18.
end do19.
end do20.
21.
do i = 1, m
22.
do j = 1, n23.
c(i, j) = 0.2 + i - j
24.
end do25.
end do26.
end subroutine init
27.
28.
29.
program main30.
31. #if defined(MKL_ILP64)
32.
use onemkl_blas_omp_offload_ilp64
33. #else34.
use onemkl_blas_omp_offload_lp64
35. #endif36.
use omp_lib
37.
use iso_fortran_env

254

oneAPI GPU 最適化ガイド
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97. 120
98. 130
99.
100.
101.
102.
103.
104.
105.

implicit none
integer, parameter
integer, parameter
integer, parameter
integer, parameter
real, allocatablereal
integer
double precision:: m = 1024:: k = 1024:: n = 1024:: iter = 2000:: a(:, :), b(:, :), c(:, :):: alpha, beta, sum, total:: i, j1, j2:: t0, t1

allocate( a(1 : m, 1 : k) )allocate( b(1 : k, 1 : n) )allocate( c(1 : m, 1 : n) )! Initialize.
alpha = 1.025
beta = 1.0
total = 0.0
call init (a, b, c, m, k, n)! Compute sgemm on the device.
t0 = omp_get_wtime()!$omp target data map(to: a, b, c)do i = 1, iter
! Update arrays a and b on the host.
a(:,:) = a(:,:) + 1b(:,:) = b(:,:) - 1! Copy new values of a and b to the device.
!$omp target update to (a, b)! Compute sgemm on the device.
!$omp dispatch
call sgemm('n','n',m,n,k,alpha,a,m,b,k,beta,c,m)
sum = 0.0!$omp target teams distribute parallel do collapse(2) reduction(+:sum)
do j1 = 1, m
do j2 = 1, n
sum = sum + c(j1,j2)
enddo
enddo
!$omp end target teams distribute parallel dototal = total + sum
end do!$omp end target data
t1 = omp_get_wtime()
print *, "total = ", total
write (*, 120) " Number of iterations = ", iterwrite (*, 130) " Time = ", t1-t0, " seconds"
format (A, I4)
format (A, F10.3, A)! Deallocate arrays.
deallocate(a)deallocate(b)deallocate(c)end program main

255

oneAPI GPU 最適化ガイド

5-15-7-5-3

バージョン 3

バージョン 3 では、ホストのみ、デバイスのみ、またはホストとデバイスの両方で利用できる行列を検討します。また、プログラムの実行中に行列の値が更新されることも考慮します。これは、行列を割り当てる場所、配列を初期化する
方法、およびデバイスで配列の値を更新するかどうか決定するのに役立ちます。
行列 a と b はホストとデバイスの両方でアクセスされるため、ホストの統合共有メモリー (USM) に割り当ててから、
map 句を使用してデバイスにマップします。
行列 c はデバイス上のワーク行列として使用され (sgemm の呼び出し結果を格納する)、ホストからはアクセスされません。そのため、デバイス上で直接割り当てます。
c はデバイス上に割り当てられているため、OpenMP* target 構造で行列を初期化するために init_d() を呼び出します。
1. !$omp allocators allocate(allocator(omp_target_shared_mem_alloc): a)
2. allocate( a(1 : m, 1 : k) )3.
4. !$omp allocators allocate(allocator(omp_target_shared_mem_alloc): b)
5. allocate( b(1 : k, 1 : n) )6.
7. !$omp allocators allocate(allocator(omp_target_device_mem_alloc): c)
8. allocate( c(1 : m, 1 : n) )9. ...
10.
11. !$omp target data map(to: a, b)
12. ...
13.
14. ! Copy new values of a and b to the device.
15. !$omp target update to (a, b)

プログラムの完全なバージョン 3 を以下に示します。
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.

256

include "mkl_omp_offload.f90"
subroutine init (a, b, m, k, n)
implicit none
real :: a(m, k), b(k,n)
integer m, k, n, i, j
do i = 1, m
do j = 1, ka(i, j) = i
end doend dodo i = 1, k
do j = 1, nb(i, j) = j - 1
end doend doend subroutine init
subroutine init_d (c, m, n)
implicit none
real :: c(m, n)
integer m, k, n, i, j!$omp target teams distribute parallel dodo i = 1, m
do j = 1, n

oneAPI GPU 最適化ガイド30.
c(i, j) = 0.2 + i - j
31.
end do32.
end do33.
end subroutine init_d
34.
35.
36.
program main37.
38. #if defined(MKL_ILP64)
39.
use onemkl_blas_omp_offload_ilp64
40. #else41.
use onemkl_blas_omp_offload_lp64
42. #endif43.
use omp_lib
44.
use iso_fortran_env
45.
implicit none
46.
47.
integer, parameter :: m = 1024
48.
integer, parameter :: k = 1024
49.
integer, parameter :: n = 1024
50.
integer, parameter :: iter = 2000
51.
real, allocatable:: a(:, :), b(:, :), c(:, :)52.
real:: alpha, beta, sum, total
53.
integer:: i, j1, j2
54.
double precision:: t0, t1
55.
56.
!$omp allocators allocate(allocator(omp_target_host_mem_alloc): a)57.
allocate( a(1 : m, 1 : k) )58.
59.
!$omp allocators allocate(allocator(omp_target_host_mem_alloc): b)60.
allocate( b(1 : k, 1 : n) )61.
62.
!$omp allocators allocate(allocator(omp_target_device_mem_alloc): c)63.
allocate( c(1 : m, 1 : n) )64.
65.
! Initialize.
66.
67.
alpha = 1.025
68.
beta = 1.0
69.
total = 0.070.
call init (a, b, m, k, n)
71.
call init_d (c, m, k, n)
72.
73.
! Compute sgemm on the device.
74.
75.
t0 = omp_get_wtime()
76.
77.
!$omp target data map(to: a, b)
78.
79.
do i = 1, iter80.
! Update arrays a and b on the host.
81.
a(:,:) = a(:,:) + 182.
b(:,:) = b(:,:) - 1
83.
84.
! Copy new values of a and b to the device.
85.
!$omp target update to (a, b)
86.
87.
! Compute sgemm on the device.
88.
!$omp dispatch
89.
call sgemm('n','n',m,n,k,alpha,a,m,b,k,beta,c,m)
90.
91.
sum = 0.092.
!$omp target teams distribute parallel do collapse(2) reduction(+:sum)93.
do j1 = 1, m
94.
do j2 = 1, n
95.
sum = sum + c(j1,j2)
96.
enddo
97.
enddo
98.
!$omp end target teams distribute parallel do

257

oneAPI GPU 最適化ガイド
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110. 120111. 130
112.
113.
114.
115.
116.
117.
118.

5-15-7-5-4

total = total + sum
end do!$omp end target data
t1 = omp_get_wtime()
print *, "total = ", total
write (*, 120) " Number of iterations = ", iterwrite (*, 130) " Time = ", t1-t0, " seconds"
format (A, I4)
format (A, F10.3, A)! Deallocate arrays.
deallocate(a)deallocate(b)deallocate(c)end program main

パフォーマンスの比較

次のコマンドで、それぞれのバージョンをコンパイル、リンク、そして実行します。以下の例の TEST.f、TEST.o、
TEST.exe は、適切なファイル名に置き換えてください。
コンパイル:$ ifx -O3 -fiopenmp -fopenmp-targets=spir64 -DMKL -qmkl -fpp -free -D DO_UPDATE_ARRAYSTEST.f -c -o TEST.o

実行:$ OMP_TARGET_OFFLOAD=MANDATORY ZE_AFFINITY_MASK=0 ./TEST.exe

特定の GPU (1 スタックのみ) で実行した 3 つのバージョンを比較します。
それぞれのバージョンの平均反復時間は次のようになりました。


バージョン 1 (test-SharedMem.f): 4.583 seconds
バージョン 2 (test-Map-UpdateTo.f): 2.157 seconds
バージョン 3 (test-DeviceMem-Map-UpdateTo.f): 1.340 seconds

5-15-7-6

節 : is_device_ptr 、 use_device_ptr 、 has_device_addr 、
use_device_addr

is_device_ptr、use_device_ptr、has_device_addr、および use_device_addr 節は、target、target
data、または dispatch 構造で参照する変数に関する情報を伝達するために使用します。これらの節の説明を以下に示します。

5-15-7-6-1

is_device_ptr

is_device_ptr 節は target または dispatch ディレクティブで指定されます。リスト項目がデバイスポインターであることを示します。そのため、各リスト項目は構造内でプライベート化され、新しいリスト項目は元のリスト項目を参照するデバイスアドレスで初期化されます。
258

oneAPI GPU 最適化ガイド•••

C 言語では、各リスト項目はポインターもしくは配列タイプである必要があります。
C++ 言語では、各リスト項目はポインター、配列、ポインター参照、または配列参照タイプである必要があります。
Fortran 言語では、各リスト項目は C_PTR である必要があります。

次の C/C++ の例では、is_device_ptr 節の使い方を示しています。omp_target_alloc_device ルーチンは、デバイス上にメモリー領域を確保し、そのメモリー領域のポインターを返してホスト変数 arr_device に保存します。
target ディレクティブで is_device_ptr(arr_device) 節を指定して、arr_device がデバイスメモリーを指していることを示します。そのため、target 構造内では arr_device はプライベート化され、arr_device が参照
するデバイスのアドレスで初期化されます。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <stdint.h>
4. #include <math.h>
5. #include <omp.h>
6.
7. #define N 100
8.
9. int main(void)
10. {
11.
int *arr_host = NULL;
12.
int *arr_device = NULL;
13.
14.
arr_host = (int *) malloc(N * sizeof(int));
15.
arr_device = (int *) omp_target_alloc_device(N * sizeof(int),
16.
omp_get_default_device());
17.
18.
#pragma omp target is_device_ptr(arr_device) map(from: arr_host[0:N])
19.
{
20.
for (int i = 0; i < N; ++i) {21.
arr_device[i] = i;22.
arr_host[i] = arr_device[i];
23.
}
24.
}
25.
26.
printf ("%d, %d, %d \n", arr_host[0], arr_host[N/2], arr_host[N-1]);
27. }5-15-7-6-2

use_device_ptr

use_device_ptr 節は target data ディレクティブで指定されます。各リスト項目が、デバイス上のストレージであるか、デバイス上でアクセスできるオブジェクトへのポインターであることを示します。
リスト項目がデバイスにマップされたオブジェクトへのポインターである場合、構造内のリスト項目への参照は構造
内ではローカルで、対応するオブジェクトのデバイスアドレスを参照するデバイスポインターへの参照に変換されます。
リスト項目がマップされたオブジェクトを指していない場合、有効なデバイスアドレスを保持している必要があり、リスト項目への参照は、このデバイスを参照するローカル・デバイス・ポインターへの参照に変換されます。
各リスト項目は、デバイス環境に対応するストレージを持つ、またはターゲットデバイス上でアクセス可能なオブジェ
クトのアドレスを持つポインターでなければなりません。
C 言語では、各リスト項目はポインターもしくは配列タイプである必要があります。
C++ 言語では、各リスト項目はポインター、配列、ポインター参照、または配列参照タイプである必要があります。

259

oneAPI GPU 最適化ガイド
Fortran 言語では、各リスト項目は C_PTR である必要があります。
次の C/C++ の例では、use_device_ptr 節の使い方を示しています。デバイス上にメモリーを割り当てるため、
omp_target_alloc_device ルーチンが 3 回呼び出されています。割り当てられたメモリーアドレスは、ホスト上のポインター変数 A、B および C に保持されます。target data ディレクティブに use_device_ptr(A, B, C)節を指定して、A、B、C が有効なデバイスアドレスを持っていることを明示します。
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <stdint.h>
4. #include <math.h>
5. #include <omp.h>
6.
7. #define length 65536
8.
9. int main(void)
10. {
11.
int device_id = omp_get_default_device();12.
size_t bytes = length*sizeof(double);13.
double * __restrict A;
14.
double * __restrict B;
15.
double * __restrict C;
16.
double scalar = 3.0;
17.
double ar;18.
double br;
19.
double cr;
20.
double asum;
21.
22.
// デバイスメモリーに配列を割り当て
23.
24.
A = (double *) omp_target_alloc_device(bytes, device_id);25.
if (A == NULL){
26.
printf(" ERROR: Cannot allocate space for A using
omp_target_alloc_device().\n");27.
exit(1);
28.
}
29.
30.
B = (double *) omp_target_alloc_device(bytes, device_id);31.
if (B == NULL){
32.
printf(" ERROR: Cannot allocate space for B using
omp_target_alloc_device().\n");33.
exit(1);
34.
}
35.
36.
C = (double *) omp_target_alloc_device(bytes, device_id);37.
if (C == NULL){
38.
printf(" ERROR: Cannot allocate space for C using
omp_target_alloc_device().\n");39.
exit(1);
40.
}
41.
42.
#pragma omp target data use_device_ptr(A,B,C)
43.
{
44.
// 配列を初期化
45.
46.
#pragma omp target teams distribute parallel for47.
for (size_t i=0; i<length; i++) {48.
A[i] = 2.0;49.
B[i] = 2.0;50.
C[i] = 0.0;
51.
}
52.
53.
// 計算を実行
54.
55.
#pragma omp target teams distribute parallel for

260

oneAPI GPU 最適化ガイド
56.
for (size_t i=0; i<length; i++) {57.
C[i] += A[i] + scalar * B[i];
58.
}
59.
60.
// 結果を評価して出力61.
62.
ar = 2.0;63.
br = 2.0;64.
cr = 0.0;65.
for (int i=0; i<length; i++) {66.
cr += ar + scalar * br;
67.
}
68.
69.
asum = 0.0;70.
#pragma omp target teams distribute parallel for reduction(+:asum)71.
for (size_t i=0; i<length; i++) {72.
asum += fabs(C[i]);
73.
}
74.
75.
} // end target data
76.
77.
omp_target_free(A, device_id);78.
omp_target_free(B, device_id);79.
omp_target_free(C, device_id);80.
81.
double epsilon=1.e-8;
82.
if (fabs(cr - asum)/asum > epsilon) {83.
printf("Failed Validation on output array\n"84.
"
Expected checksum: %lf\n"85.
"
Observed checksum: %lf\n"86.
"ERROR: solution did not validate\n", cr, asum);87.
return 1;
88.
} else {89.
printf("Solution validates. Checksum = %lf\n", asum);
90.
}
91.
92.
return 0;
93. }5-15-7-6-3

has_device_addr

has_device_addr 節は target ディレクティブで指定されます。これは、リスト項目がすでに有効なデバイスアドレスを保持しており、デバイスから直接アクセスできることを示します。
各リスト項目は、デバイスデータ環境の有効なデバイスアドレスを持っている必要があります。配列セクションを含む
すべてのタイプに使用できます。
has_device_addr 節は Fortran では特に有用であり、C_PTR に限らずあらゆるタイプのリスト項目で使用でき、リスト項目がデバイスアドレスであることを明示できます。
次の Fortran コードは、has_device_addr 節の使い方を示します。この例では、3 つの配列 A、B および C をデバイ
ス上に割り当てています。has_device_addr 領域で配列が参照される場合、has_device_addr(A, B, C) 節を指定して A、B、C がすでにデバイスアドレスを持っていることを明示します。
1.
2.
3.
4.
5.
6.
7.
8.

program mainuse iso_fortran_env
use omp_lib
implicit none
integer, parameter :: iterations=1000
integer, parameter :: length=64*1024*1024
real(kind=REAL64), parameter :: epsilon=1.D-8

261

oneAPI GPU 最適化ガイド
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.

real(kind=REAL64), allocatable :: A(:)
real(kind=REAL64), allocatable :: B(:)
real(kind=REAL64), allocatable :: C(:)
real(kind=REAL64) :: scalar=3.0
real(kind=REAL64) :: ar, br, cr, asumreal(kind=REAL64) :: nstream_time, avgtime
integer :: i, iter!
! デバイスメモリーに配列を割り当て!$omp allocate (allocator(omp_target_device_mem_alloc): A)
allocate(A(length))!$omp allocate (allocator(omp_target_device_mem_alloc): B)
allocate(B(length))!$omp allocate (allocator(omp_target_device_mem_alloc): C)
allocate(C(length))!
! 配列の初期化!$omp target teams distribute parallel do has_device_addr(A, B, C)
do i = 1, length
A(i) = 2.0
B(i) = 2.0
C(i) = 0.0
end do!
! 計算を実行nstream_time = omp_get_wtime()
do iter = 1, iterations!$omp target teams distribute parallel do has_device_addr(A, B, C)
do i = 1, length
C(i) = C(i) + A(i) + scalar * B(i)end doend donstream_time = omp_get_wtime() - nstream_time!
! 結果を評価して出力
ar = 2.0
br = 2.0
cr = 0.0
do iter = 1, iterations
do i = 1, length
cr = cr + ar + scalar * brend doend doasum = 0.0!$omp target teams distribute parallel do reduction(+:asum) has_device_addr(C)do i = 1, length
asum = asum + abs(C(i))
end doif (abs(cr - asum)/asum > epsilon) thenprint *, "Failed Validation on output array:", "Expected =", cr, "Observed =",
asum
71.
else
72.
avgtime = nstream_time/iterations
73.
print *, "Solution validates:", "Checksum =", asum, "Avg time (s) =", avgtime
74.
endif

262

oneAPI GPU 最適化ガイド
75.
76.
deallocate(A)77.
deallocate(B)78.
deallocate(C)79.
80. end program main

5-15-7-6-4

use_device_addr

use_device_addr 節は target data ディレクティブで指定されます。リスト項目がデバイス上のストレージであるか、またデバイス上でアクセス可能であることを示します。
リスト項目がマップされている限り、構造内のリスト項目の参照は対応するリスト項目への参照に変換されます。リス
ト項目がマップされていない場合、デバイス上でアクセス可能であるとみなされます。
リスト項目は配列セクションであってもかまいません。
has_device_addr と同様に、use_device_addr 節は Fortran では特に有用であり、C_PTR に限らずあらゆるタ
イプのリスト項目で使用でき、リスト項目がデバイスアドレスであることを明示できます。
次の Fortran コードは、use_device_addr 節の使い方を示します。この例では、array_d が alloc マップタイプ
でデバイスにマップされているため、デバイス上に array_d のストレージが確保されますが、ホストとデバイス間でデータ転送は発生し ません。 target data ディレクティブで use_device_addr(array_d) 節を指定し、array_d がデバイス上にストレージを持っていることを示します。
1. program target_use_device_addr
2.
3.
use omp_lib
4.
use iso_fortran_env, only : real64
5.
implicit none
6.
7.
integer, parameter :: N1 = 10248.
real(kind=real64), parameter :: aval = real(42, real64)
9.
real(kind=real64), allocatable :: array_d(:), array_h(:)
10.
integer :: i,err
11.
12.
! Allocate host data
13.
allocate(array_h(N1))
14.
15.
!$omp target data map (from:array_h(1:N1)) map(alloc:array_d(1:N1))
16.
!$omp target data use_device_addr(array_d)
17.
!$omp target18.
do i=1, N1
19.
array_d(i) = aval
20.
array_h(i) = array_d(i)21.
end do22.
!$omp end target
23.
!$omp end target data24.
!$omp end target data
25.
26.
! 結果をチェック27.
write (*,*) array_h(1), array_h(N1)28.
if (any(array_h /= aval)) then29.
err = 1
30.
else
31.
err = 032.
end if33.
34.
deallocate(array_h)35.
if (err == 1) then36.
stop 1263

oneAPI GPU 最適化ガイド
37.
else
38.
stop 039.
end if40.
41. end program target_use_device_addr

次の表は、このセクションで説明する節の特性をまとめたものです。
節
is_device_ptr

ディレクティブ
target、dispatch

use_device_ptr

target data

has_device_addr

targetuse_device_addr

target data

5-15-7-7

リスト項目のタイプ
C/C++: ポインター、配列または参照Fortran: C_PTR
C/C++:ポインター、配列または参照
Fortran: C_PTR

任意のタイプ (配列セクションでも可能)
任意のタイプ (配列セクションでも可能)

説明
リスト項目がデバイスポインターであることを示します (有効なデバ
イスアドレスを持つ)。
リスト項目がデバイス上のストレージであるか、デバイス上でアクセスできるオブジェクトへのポイ
ンターであることを示します。
リスト項目が有効なデバイスアドレスを持つことを示します。
リスト項目がデバイス上のストレージであるか、デバイス上でアクセス可能であることを示します。

プリフェッチユーザーガイドに示されるデータ・プリフェッチ方法は、下位レベルのキャッシュミスやメインメモリーのアクセスから生じるレイテンシーを隠匿するのに役立ちます。インテル® GPU 向けの OpenMP* オフロードでは、次の構文でprefetch プラグマを指定することでこの機能を有効にできます。
C OpenMP* プリフェッチ:#pragma ompx prefetch data([prefetch-hint-modifier:],arrsect,[,arrsect])[if(condition)]

Fortran OpenMP* プリフェッチ:!$omp prefetch data([prefetch-hint-modifier:] arrsect [,arrsect])[if(condition)]

この prefetch プラグマはインテルの拡張機能であり、インテル® データセンター GPU マックス・シリーズ以降の製品で機能します。プラグマのパラメーターは次のとおりです。
•

Prefetch-hint: プリフェッチされたデータの格納先は、オプションの prefetch-hint-modifier で指定します。有効な値は、0 (プリフェッチしない)、2 (L2 のみにプリフェッチ) および 4 (L1 と L2 にプリフェッチ) です。値が省略されるとデフォルト値は 0 です。

•

配列セクションを使用: C では OpenMP* 構文 [lower-bound : length] を使用して、連続する配列セク
ション arrsect を指定します。例えば、C では a[2:4]、Fortran では (lower-bound: upper-bound) を使用します。例えば、a[10] または a(10) で始まる 4 つの要素をプリフェッチするには、C では a[10:4[ を、Fortranでは a(10:13) を使用します。このドキュメントの執筆時点では、コンパイラーは単一要素のプリフェッチを要求することに注意してください。したがって、上記の例は、C の a[10:1]、Fortran の a（10:10） に相当します。

•

デフォルト・プリフェッチ・サイズ: 配列の単一要素のプリフェッチが要求された場合でも、ハードウェアはその要素を含むキャッシュライン全体をプリフェッチします。インテル® データセンター GPU マックス・シリーズのキャッシュラインのサイズは 64 バイトです。

264

oneAPI GPU 最適化ガイド•

フォールト: インテル® データセンター GPU マックス・シリーズのプリフェッチ命令でエラーが発生し、無効なア
ドレスへのアクセスによりセグメンテーション・フォールトが発生する可能性があることを意味します。プラグマの if 節に condition を指定して、範囲外のアクセスを保護できます。

•

非ブロック: prefetch プラグマは実行をブロックせず、プリフェッチが完了するのを待機しません。

5-15-7-7-1

C OpenMP* のプリフェッチ

次の例は、N 体フォースカーネルを簡略化した 1 次元バージョンを示しています。外部 for ループは、力が計算される粒子に対し反復します。内部ループは、TILE_SIZE 粒子のバッチで相互に作用する粒子を反復処理します。現在の粒子タイルの計算中に、次のスタックをプリフェッチできます。前述したように、プリフェッチは常に 64 バイトを読み取ります。したがって、if ( (next_tile % 16) == 0 ) によって、16 個の単精度浮動小数点要素ごとに 1 回
プリフェッチを発行します。このマスク条件が常に有効であるとは限りません。以下のコード例の後にある追加の注意事項を参照してください。prefetch hint には 4 を使用します (L1 と L2 キャッシュへプリフェッチ)。以下には、オフロードカーネルのみを示します。
1. #define WORKGROUP_SIZE 1024
2. #define PREFETCH_HINT 4 // 4 = L1 と L3 へプリフェッチ; 2 = L3 へプリフェッチ
3. #define TILE_SIZE 64
4.
5. void nbody_1d_gpu(float *c, float *a, float *b, int n1, int n2) {6. #pragma omp target teams distribute parallel for thread_limit(WORKGROUP_SIZE)
7.
for (int i = 0; i < n1; i++) {8.
const float ma0 = 0.269327f, ma1 = -0.0750978f, ma2 = 0.0114808f;
9.
const float ma3 = -0.00109313f, ma4 = 0.0000605491f, ma5 = -0.00000147177f;
10.
const float eps = 0.01f;
11.
12.
float dx = 0.0;
13.
float bb[TILE_SIZE];
14.
for (int j = 0; j < n2; j += TILE_SIZE) {15.
// タイルを b からロード
16.
for (int u = 0; u < TILE_SIZE; ++u) {17.
bb[u] = b[j + u];
18. #ifdef PREFETCH
19.
int next_tile = j + TILE_SIZE + u;20.
if ((next_tile % 16) == 0) {21. #pragma ompx prefetch data(PREFETCH_HINT : b[next_tile]) if (next_tile < n2)
22.
}
23. #endif
24.
}
25. #pragma unroll(TILE_SIZE)
26.
for (int u = 0; u < TILE_SIZE; ++u) {27.
float delta = bb[u] - a[i];28.
float r2 = delta * delta;
29.
float s0 = r2 + eps;
30.
float s1 = 1.0f / sqrtf(s0);31.
float f =32.
(s1 * s1 * s1) 33.
(ma0 + r2 * (ma1 + r2 * (ma2 + r2 * (ma3 + r2 * (ma4 + ma5)))));
34.
dx += f * delta;
35.
}
36.
}
37.
c[i] = dx * 0.23f;
38.
}
39. }

265

oneAPI GPU 最適化ガイド
条件 if ( (next_tile % 16) == 0 ) では、配列インデックスがベクトル化されていない場合、プリフェッチのオーバーヘッドを軽減できます。上記の例では、インデックス i のみがベクトル化されるため、j によってインデックス
付けされた b[] をプリフェッチする場合に、16 要素ごとに一度だけプリフェッチを発行します。一方、インデックス i
によって配列をプリフェッチすると、プリフェッチはベクトル化されるため、マスクの条件は何の利点ももたらしません。
ユーザーは、アプリケーションに最適なアプローチを実験的に決定する必要があります。
コンパイルコマンド
プリフェッチなし:$ icpx -O3 -g -fiopenmp -fopenmp-targets=spir64 -mcmodel=medium nbody_c.cpp -o test_c

プリフェッチあり:$ icpx -O3 -g -fiopenmp -fopenmp-targets=spir64 -mcmodel=medium -DPREFETCH nbody_c.cpp-o test_c

実行コマンド$ LIBOMPTARGET_LEVEL0_COMPILATION_OPTIONS="-cl-strict-aliasing -cl-fast-relaxed-math"\
ZE_AFFINITY_MASK=0 LIBOMPTARGET_PLUGIN_PROFILE=T,usec GC_ForceOCLSIMDWidth=16 \./test_c

デフォルトの SIMD 幅は、インテル® データセンター GPU マックス・シリーズ向けのバックエンド・デバイス・コン
パイラー (インテル® グラフィックス・コンパイラーまたは IGC) によって、カーネルのレジスター・プレッシャーなどの要因を考慮したコンパイラーのヒューリスティックによって 16 または 32 に自動設定されます。IGC 環境変数
IGC_ForceOCLSIMDWidth=16 を使用して、IGC コンパイラーに SIMD 幅 16 を強制できます。SIMD16 に設定することで、上記カーネルのパフォーマンスを向上できました。コマンドの実行時に、
LIBOMPTARGET_PLUGIN_PROFILE=T,usec を設定して、OpenMP* ビルトイン・プロファイラーを有効にします。プリフェッチなしで実行した出力は次のとおりです。
Obtained output = 222700231.430
Expected output = 222700339.016
Total time =205.4 milliseconds======================================================================================================================
LIBOMPTARGET_PLUGIN_PROFILE(LEVEL0) for OMP DEVICE(0) Intel(R) Graphics [0x0bd6],
Thread 0--------------------------------------------------------------------------------------------------------------------Kernel 0: __omp_offloading_46_3c0d785c__Z12nbody_1d_gpuPfS_S_ii_l15
Kernel 1: __omp_offloading_46_3c0d785c__Z15clean_cache_gpuPdi_l69
Kernel 2: __omp_offloading_46_3c0d785c__Z4main_l98--------------------------------------------------------------------------------------------------------------------: Host Time (usec)
Device Time (usec)
Name
:
Total
Average
Min
Max
Total
Average
Min
Max
Count
--------------------------------------------------------------------------------------------------------------------Compiling: 598283.05 598283.05 598283.05 598283.05
0.00
0.00
0.00
0.00
1.00
DataAlloc:
9578.23798.190.00
8728.03
0.00
0.00
0.00
0.00
12.00
DataRead (Device to Host) :
77.01
77.01
77.01
77.01
5.68
5.68
5.68
5.68
1.00
DataWrite (Host to Device):
713.11356.55179.05534.06
15.76
7.88
5.04
10.72
2.00

266

oneAPI GPU 最適化ガイド
Kernel 0: 205292.22
2052.922033.95
2089.98 203572.32
2035.72
1984.962073.12100.00Kernel 1: 109194.28
1091.94
1076.941681.09 107051.52
1070.52
1062.40
1107.04
100.00Kernel 2:
1746.89
1746.89
1746.89
1746.89
3.84
3.84
3.84
3.84
1.00
Linking
:
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
1.00
OffloadEntriesInit:
2647.882647.882647.882647.88
0.00
0.00
0.00
0.00
1.00
======================================================================================================================

上記の出力では、GPU カーネル実行 (Kernel 0) の平均デバイス時間は 2036 マイクロ秒です。プリフェッチを有効にしてバイナリーを実行すると、平均カーネルデバイス時間は 1841 マイクロ秒になりました。
Obtained output = 222700231.430
Expected output = 222700339.016
Total time =185.9 milliseconds======================================================================================================================
LIBOMPTARGET_PLUGIN_PROFILE(LEVEL0) for OMP DEVICE(0) Intel(R) Graphics [0x0bd6],
Thread 0--------------------------------------------------------------------------------------------------------------------Kernel 0: __omp_offloading_43_3c0d785c__Z12nbody_1d_gpuPfS_S_ii_l15
Kernel 1: __omp_offloading_43_3c0d785c__Z15clean_cache_gpuPdi_l69
Kernel 2: __omp_offloading_43_3c0d785c__Z4main_l98--------------------------------------------------------------------------------------------------------------------: Host Time (usec)
Device Time (usec)
Name
:
Total
Average
Min
Max
Total
Average
Min
Max
Count
--------------------------------------------------------------------------------------------------------------------Compiling: 499351.98 499351.98 499351.98 499351.98
0.00
0.00
0.00
0.00
1.00
DataAlloc:
9609.94800.830.00
8740.19
0.00
0.00
0.00
0.00
12.00
DataRead (Device to Host) :
77.01
77.01
77.01
77.01
4.96
4.96
4.96
4.96
1.00
DataWrite (Host to Device):
722.17361.08185.01537.16
16.40
8.20
5.44
10.96
2.00
Kernel 0: 185793.88
1857.94
1839.88
1919.03 184075.20
1840.75
1824.00
1874.56
100.00Kernel 1: 109442.95
1094.43
1076.94
1590.01 107334.56
1073.35
1062.401115.68
100.00Kernel 2:1821.99
1821.99
1821.99
1821.99
3.84
3.84
3.84
3.84
1.00
Linking
:
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
1.00
OffloadEntriesInit:
2493.142493.142493.142493.14
0.00
0.00
0.00
0.00
1.00
======================================================================================================================

達成されるパフォーマンスは、利用するハードウェアとソフトウェア・スタックに依存するため、ユーザー環境では異なるパフォーマンス値となる可能性があることに留意してください。

267

oneAPI GPU 最適化ガイド

5-15-7-7-2

Fortran OpenMP* のプリフェッチ

同じ nbody1d カーネルを Fortran で見てみます。prefetch ディレクティブは、PREFETCH_HINT 4 で C の例と同じ位置に挿入され、16 要素ごとに 1 回のプリフェッチを行います。
1. #define WORKGROUP_SIZE 1024
2. #define PREFETCH_HINT 4! 4 = prefetch to L1 and L3; 2 = prefetch to L3
3. #define TILE_SIZE 64
4.
5.
subroutine nbody_1d_gpu(c, a, b, n1, n2)
6.
implicit none
7.
integer n1, n2
8.
real a(0:n1-1), b(0:n2-1), c(0:n1-1)
9.
real dx, bb(0:TILE_SIZE-1), delta, r2, s0, s1, f
10.
integer i,j,u,next
11.
real ma0, ma1, ma2, ma3, ma4, ma5, eps
12.
parameter (ma0=0.269327, ma1=-0.0750978, ma2=0.0114808)
13.
parameter (ma3=-0.00109313, ma4=0.0000605491, ma5=-0.00000147177)
14.
parameter (eps=0.01)
15.
16. !$omp target teams distribute parallel do thread_limit(WORKGROUP_SIZE)
17. !$omp& private(i,dx,j,u,bb,next,delta,r2,s0,s1,f)
18.
do i = 0, n1-1
19.
dx = 0.020.
do j = 0, n2-1, TILE_SIZE
21.
! タイルを b からロード
22.
do u = 0, TILE_SIZE-1
23.
bb(u) = b(j+u)
24. #ifdef PREFETCH
25.
next = j + TILE_SIZE + u
26.
if (mod(next,16).eq.0) then
27. !$omp prefetch data(PREFETCH_HINT:b(next:next))if(next<n2)
28.
endif
29. #endif
30.
enddo
31.
! 計算
32.
!DIR$ unroll(TILE_SIZE)
33.
do u = 0, TILE_SIZE-1
34.
delta = bb(u) - a(i)35.
r2 = delta*delta36.
s0 = r2 + eps
37.
s1 = 1.0 / sqrt(s0)38.
f = (s1*s1*s1)-(ma0+r2*(ma1+r2*(ma2+r2*(ma3+r2*(ma4+ma5)))))
39.
dx = dx + f*delta
40.
enddo
41.
enddo
42.
c(i) = dx*0.23
43.
enddo
44.
end subroutine

コンパイルコマンド
プリフェッチなし:$ ifx -O3 -g -fiopenmp -fopenmp-targets=spir64 -fpconstant -fpp -ffast-math -fno-syclinstrument-device-code -mcmodel=medium nbody_f.f -o test_f

プリフェッチあり:$ ifx -O3 -g -fiopenmp -fopenmp-targets=spir64 -fpconstant -fpp -ffast-math -fno-syclinstrument-device-code -mcmodel=medium -DPREFETCH nbody_f.f -o test_f268

oneAPI GPU 最適化ガイド
実行コマンド$ LIBOMPTARGET_LEVEL0_COMPILATION_OPTIONS="-cl-strict-aliasing -cl-fast-relaxed-math"\
ZE_AFFINITY_MASK=0 LIBOMPTARGET_PLUGIN_PROFILE=T,usec IGC_ForceOCLSIMDWidth=16 \./test_f

C の例と類似しているため、ここでは出力を示しません。プリフェッチを使用しない場合と使用した場合の平均カーネ
ル時間は、それぞれ 2017μs と 1823μs でした。繰り返しますが、実際のハードウェアと利用するソフトウェア・スタッ
クによって、異なるパフォーマンス値となる可能性を忘れないでください。

5-15-7-7-3

C OpenMP* SIMD のプリフェッチ

OpenMP* オフロードは、インテル® データセンター GPU マックス・シリーズの 16 または 32 SIMD レーンを構成するベクトルエンジンのスレッドで、すべての計算を指定する SIMD プログラミング・モデルをサポートしています。これに対応して、OpenMP* の thread_limit() 節の意味も変わり、work-group ごとのベクトルエンジンのスレッド数を指定するようになりました。OpenMP* SIMD バージョンの nbody1d カーネルを以下に示します。VECLEN=16
で SIMD 幅を明示的に指定する必要があります。現時点では、prefetch プラグマは simd 句の範囲外で指定することを推奨します。これは、1 つの SIMD レーンのみがプリフェッチを発行することを意図しています。この例では、16レーンのうち 1 レーンがプリフェッチを発行します。そのため、前述した if ( (next_tile % 16) == 0 ) 節は
ここでは必要ありません。
1. #define WORKGROUP_SIZE 1024
2. #define PREFETCH_HINT 4 // 4 = L1 と L3 へプリフェッチ; 2 = L3 へプリフェッチ
3. #define TILE_SIZE 64
4.
5. void nbody_1d_gpu(float *c, float *a, float *b, int n1, int n2) {6. #pragma omp target teams distribute parallel for thread_limit(WORKGROUP_SIZE / \7.
VECLEN)8.
for (int i = 0; i < n1; i += VECLEN) {9.
const float ma0 = 0.269327f, ma1 = -0.0750978f, ma2 = 0.0114808f;
10.
const float ma3 = -0.00109313f, ma4 = 0.0000605491f, ma5 = -0.00000147177f;
11.
const float eps = 0.01f;
12.
13.
float dx[VECLEN];
14.
float aa[VECLEN], bb[TILE_SIZE];
15. #pragma omp simd simdlen(VECLEN)
16. #pragma unroll(0)17.
for (int v = 0; v < VECLEN; ++v) {18.
dx[v] = 0.0f;
19.
aa[v] = a[i + v];
20.
}
21.
for (int j = 0; j < n2; j += TILE_SIZE) {22.
// タイルを b からロード
23.
for (int u = 0; u < TILE_SIZE; u += VECLEN) {24. #pragma omp simd simdlen(VECLEN)
25. #pragma unroll(0)26.
for (int v = 0; v < VECLEN; ++v)
27.
bb[u + v] = b[j + u + v];
28. #ifdef PREFETCH
29.
int next_tile = j + TILE_SIZE + u;
30. #pragma ompx prefetch data(PREFETCH_HINT : b[next_tile]) if (next_tile < n2)
31. #endif
32.
}
33. // 現在のタイルを計算
34. #pragma omp simd simdlen(VECLEN)
35. #pragma unroll(0)36.
for (int v = 0; v < VECLEN; ++v) {37. #pragma unroll(TILE_SIZE)
38.
for (int u = 0; u < TILE_SIZE; ++u) {39.
float delta = bb[u] - aa[v];269oneAPI GPU 最適化ガイド
40.
float r2 = delta * delta;
41.
float s0 = r2 + eps;
42.
float s1 = 1.0f / sqrtf(s0);43.
float f =44.
(s1 * s1 * s1) 45.
(ma0 + r2 * (ma1 + r2 * (ma2 + r2 * (ma3 + r2 * (ma4 + ma5)))));
46.
dx[v] += f * delta;
47.
}
48.
}
49.
}
50. #pragma omp simd simdlen(VECLEN)
51. #pragma unroll(0)52.
for (int v = 0; v < VECLEN; ++v) {53.
c[i + v] = dx[v] * 0.23f;
54.
}
55.
}
56. }

コンパイルコマンド
SIMD プログラミング・モデルを有効にするには、-fopenmp-target-simd コンパイルオプションを追加する必要があります。以下にコンパイルコマンドを示します。
プリフェッチなし:$ icpx -O3 -g -fiopenmp -fopenmp-targets=spir64 -mcmodel=medium \-fopenmp-target-simd nbody_c_simd.cpp -o test_c_simd

プリフェッチあり:$ icpx -O3 -g -fiopenmp -fopenmp-targets=spir64 -mcmodel=medium -DPREFETCH \-fopenmp-target-simd nbody_c_simd.cpp -o test_c_simd

実行コマンド$ LIBOMPTARGET_LEVEL0_COMPILATION_OPTIONS="-cl-strict-aliasing -cl-fast-relaxed-math"\
ZE_AFFINITY_MASK=0 LIBOMPTARGET_PLUGIN_PROFILE=T,usec ./test_c_simd

OpenMP* コードで SIMD 幅が明示的に指定されているため、環境変数 IGC_ForceOCLSIMDWidth=16 は必要ありません。
出力は前の例と類似するためここでは示しません。プリフェッチを使用しない場合と使用した場合の平均カーネル時
間は、それぞれ 2008μs と 1810μs でした。繰り返しますが、実際のハードウェアと利用するソフトウェア・スタックによって、異なるパフォーマンス値となる可能性を忘れないでください。

5-15-7-8
5-15-7-8-1

SLM とアトミック
はじめにOpenMP* の atomic ディレクティブは、指定されたコードセクションがアトミック操作として実行されることを保証します。
atomic ディレクティブを使用するには、アトミック実行が必要なコードの前に、C/C++ の #pragma omp atomic
または Fortran の同等のディレクティブを挿入します。たとえば、ディレクティブ #pragma omp atomic updateの後に x++; を続けると、変数 x のインクリメント操作がアトミックになり、この操作中に他のスレッドが x を更新できなくなります。
270

oneAPI GPU 最適化ガイド

5-15-7-8-2

SLM とアトミック

ヒストグラム・ミニプログラムを使用して、OpenMP* atomic ディレクティブの使用方法を説明します。
ベースとなるコード
ヒストグラムは、販売記録を分析して最も人気のある商品を特定するなど、統計をまとめる上で重要です。次のベース
コードを考えてみましょう。
1.
2.
3.
4.
5.
6.

#pragma omp target teams distribute parallel for map(to : input[0 : size]) \map(tofrom : result[0 : num_types])for (int i = 0; i < size; i++) {int type = input[i];result[type]++;}このコードは、OpenMP* target ディレクティブを使用して GPU にオフロードします。入力配列には販売されたアイ
テムのタイプが含まれます。タイプが見つかるたびに、結果の対応するカウントが増加します。ただし、このアプローチ
では、複数のスレッドが同じヒストグラム・エントリー (結果要素) を同時に更新するとデータ競合が発生する可能性があり、排他的アクセスを保証するメカニズムが必要になります。
critical ディレクティブ
データ競合を回避するには、他のスレッドが同じヒストグラム・エントリーを同時に更新するのをブロックするクリティ
カル・セクションを実装します。
これを強制する方法の 1 つは、OpenMP* の critical ディレクティブを使用することです。これにより、囲まれたコードがチーム内で一度に 1 つのスレッドによってのみ実行されるようになります。この排他性は、内部的に比較交換
ループを使用して実現されます。OpenMP* 仕様によると、critical ディレクティブはチーム間の排他性を強制しないため、使用できるのは 1 つのチームのみです。実装方法は次のとおりです。
1.
2.
3.
4.
5.
6.
7.

#pragma omp target teams distribute parallel for map(to : input [0:size])\map(tofrom : result [0:num_bins]) num_teams(1)for (int i = 0; i < size; i++) {int type = input[i];#pragma omp criticalresult[type]++;}atomic ディレクティブ
ヒストグラムの更新は正しくなりましたが、比較交換 (Compare-Exchange) ループで critical ディレクティブを使用すると、かなりのオーバーヘッドが発生します。これは、多数のスレッドが同時に同じターゲット値を更新しようとする可能性がある GPU で顕著です。さらに、critical ディレクティブは複数のチームを使用できません。パフォーマンスを向上させるため、指定されたアトミック操作を実行するハードウェア・サポートを必要とする OpenMP* のatomic ディレクティブを使用します。このコンテキストでは、スカラーを 1 ずつインクリメントすることは、インテル®
データセンターGPU マックス・シリーズでサポートされているアトミック操作であるため、atomic ディレクティブは実行可能なオプションになります。改善されたコードは次のとおりです。
1.
2.
3.
4.
5.
6.

#pragma omp target teams distribute parallel for map(to: input [0:size])\map(tofrom : result [0:num_bins])for (int i = 0; i < size; i++) {int type = input[i];#pragma omp atomic update
result[type]++;271

oneAPI GPU 最適化ガイド
7.

}あるいは、atomic ディレクティブにシーケンシャルな一貫性メモリー順序を指定することもできます。このシナリオ
ではわずかな違いしかありませんが、他のコンテキストではより大きな影響を与える可能性があります。コーディング
方法は次のとおりです。
1.
2.
3.
4.
5.
6.
7.

#pragma omp target teams distribute parallel for map(to : input [0:size])\map(tofrom: result [0:num_bins])for (int i = 0; i < size; i++) {int type = input[i];#pragma omp atomic update seq_cst
result[type]++;}

OpenMP* は、acquire や release などの他のメモリー順序もサポートしています。詳細については、OpenMP*
API 5.2 仕様を参照してください。
SLM と atomic ディレクティブ
OpenMP* atomic ディレクティブに基づいて、共有ローカルメモリー (SLM) を使用することでパフォーマンスをさらに向上できます。最初は、ヒストグラムへのすべてのアクセスがグローバルメモリーで行われるため、レイテンシーが高くなります。ただし、同じチーム内のスレッドは SLM のローカル・ヒストグラムを利用できるため、アクセス速度が向上します。
このプロセスでは、各チームがヒストグラムのローカル部分に取り組み、ローカルの結果をグローバル ・ヒストグラムにマージすることで、グローバルメモリーのアクセス頻度を減らします。チーム間のデータの分割は、チームの数に基づいて実行時に動的に処理され、各チームは特定の範囲のデータを処理します。ローカル・ヒストグラムをグローバ
ル・ヒストグラムにマージする場合、データ競合を防ぐため atomic ディレクティブが必要です。
#pragma omp target teams map(to : input [0:size]) map(tofrom : result [0:num_bins]){// create a local histogram using SLM in the team
int local_histogram[NUM_BINS] = {0};int num_local_histogram = omp_get_num_teams();
int team_id = omp_get_team_num();
int chunk_size = size / num_local_histogram;
int leftover = size % num_local_histogram;
int local_lb = team_id * chunk_size;
int local_ub = (team_id + 1) * chunk_size;// Add the leftover to last chunk.
// e.g. 18 iterations and 4 teams -> 4, 4, 4, 6 = 4(last chunk) +// 2(leftover)if (local_ub + chunk_size > size)
local_ub += leftover;if (local_ub <= size) {#pragma omp parallel for shared(local_histogram)for (int i = local_lb; i < local_ub; i++) {int type = input[i];#pragma omp atomic update
local_histogram[type]++;}// Combine local histograms#pragma omp parallel forfor (int i = 0; i < num_bins; i++) {#pragma omp atomic update
result[i] += local_histogram[i];
}
}
}

272

oneAPI GPU 最適化ガイド
前の例と同様に、この実装にもシーケンシャルな一貫性メモリーモデルを使用できます。
#pragma omp target map(to : input [0:size]) map(tofrom : result [0:num_bins])#pragma omp teams{// create a local histogram using SLM in the team
int local_histogram[NUM_BINS] = {0};int num_local_histogram = omp_get_num_teams();
int team_id = omp_get_team_num();
int chunk_size = size / num_local_histogram;
int leftover = size % num_local_histogram;
int local_lb = team_id * chunk_size;
int local_ub = (team_id + 1) * chunk_size;// Add the leftover to last chunk.
// e.g. 18 iterations and 4 teams -> 4, 4, 4, 6 = 4(last chunk) +// 2(leftover)if (local_ub + chunk_size > size)
local_ub += leftover;if (local_ub <= size) {#pragma omp parallel for shared(local_histogram)for (int i = local_lb; i < local_ub; i++) {int type = input[i];#pragma omp atomic update seq_cst
local_histogram[type]++;}// Combine local histograms#pragma omp parallel forfor (int i = 0; i < num_bins; i++) {#pragma omp atomic update seq_cst
result[i] += local_histogram[i];}}}5-15-7-8-3

評価

評価は、インテル® データセンターGPU MAX 1100 を使用して実施され、インテル® OneAPI ツールキット 2024.2
でコンパイルされました。
コンパイルコマンド$ icpx -O3 -fiopenmp -fopenmp-targets=spir64 histogram.cpp -o histogram.out

実行コマンド$ LIBOMPTARGET_PLUGIN_PROFILE=1 ./histogram.out

プロファイル
OpenMP* プロファイラーを使用して、5 つの異なるカーネルの実行時間を測定しました。これは、環境変数
LIBOMPTARGET_PLUGIN_PROFILE=1 を設定することで有効になりました。パフォーマンスの数値はシステムによって異なる場合があります。
...
======================================================================================================================
LIBOMPTARGET_PLUGIN_PROFILE(LEVEL_ZERO) for OMP DEVICE(0) Intel(R) Data Center GPU Max
1100, Thread 0---------------------------------------------------------------------------------------------------------------------

273

oneAPI GPU 最適化ガイド
Kernel 1: __omp_offloading_39_7be84d7a__Z4main_l45
Kernel 2: __omp_offloading_39_7be84d7a__Z4main_l62
Kernel 3: __omp_offloading_39_7be84d7a__Z4main_l79
Kernel 4: __omp_offloading_39_7be84d7a__Z4main_l96
Kernel 5: __omp_offloading_39_7be84d7a__Z4main_l137--------------------------------------------------------------------------------------------------------------------: Host Time (msec)
Device Time (msec)
Name
:
Total
Average
Min
Max
Total
Average
Min
Max
Count
--------------------------------------------------------------------------------------------------------------------...
Kernel 1
:
0.54
0.54
0.54
0.54
3647.863647.863647.863647.86
1.00
Kernel 2
:
0.06
0.06
0.06
0.06
3.05
3.05
3.05
3.05
1.00
Kernel 3
:
0.01
0.01
0.01
0.01
3.04
3.04
3.04
3.04
1.00
Kernel 4
:
0.01
0.01
0.01
0.01
0.11
0.11
0.11
0.11
1.00
Kernel 5
:
0.01
0.01
0.01
0.01
0.23
0.23
0.23
0.23
1.00
...
======================================================================================================================...

カーネルの説明
カーネル

行

説明

1

45 - 53

omp critical262 - 70

omp atomic relaxed379 - 87

omp atomic seq_cst

4

96 - 128

omp atomic relaxed with SLM

5137 - 168

omp atomic seq_cst with SLM

デバイス・アセンブリーの調査により、atomic relaxed および seq_cst メモリー順序を使用するカーネル 2 ～5 が、操作 result[type]++; に単一のデバイス命令 atomic_iadd.ugm.d32.a64 を使用していることが明らかになりました。対照的に、critical ディレクティブを使用したカーネル 1 は、同じ操作に対して 100 を超える命令を含む複雑な比較交換ループを生成しました。SLM を使用すると、atomic_iadd.slm.d32.a32 などの命令が使用され、より高速な共有ローカルメモリー内での計算が大幅に高速化されました。特に、SLM を使用するとパフォー
マンスが大幅に向上し、最大 28 倍の高速化 (3.05 ミリ秒に対して 0.11 ミリ秒) が達成されました。atomic
relaxed および seq_cst メモリー命令では、後者の方がアセンブリーでより多くのフェンスが生成されるため、予想どおりわずかに遅くなります。

274

oneAPI GPU 最適化ガイド

5-15-7-8-4

まとめ

比較を解析することにより、atomic と critical にはパフォーマンスに大きな差があることが明確です。インテル®
データセンターGPU マックス・シリーズなどのハードウェアがアトミック操作をサポートしている場合、atomic は効率と命令の複雑さの軽減による大きな利点があります。これは、加算などの単純なアトミック操作に特に当てはまります。逆に、ハードウェアで直接サポートされていないより複雑な操作の場合、critical ディレクティブを引き続き使用できますが、パフォーマンスの低下を防ぐため追加の最適化が必要になる場合があります。さらに、SLM を
atomic ディレクティブと組み合わせて活用すると、メモリーアクセスの大部分がグローバルメモリーからローカルメ
モリーに移行されるため、レイテンシーが短縮され、スループットが向上するため、パフォーマンスがさらに向上します。

5-16 マルチ GPU、マルチスタック・アーキテクチャーとプログラミング
インテル® データセンター GPU マックス・シリーズは、1 から 2 スタックのマルチスタック GPU アーキテクチャーを採用しています。この新しい GPU アーキテクチャー製品により、マルチ GPU、マルチスタック、およびマルチ C スライス計算が可能になります。
この章では、マルチ GPU、マルチスタック、およびマルチ C スライスのヘテロジニアス・コンピューティングに関する
次のトピックを説明します。
•
•
•
•
•
•
•
•

マルチスタック GPU アーキテクチャー
デバイス階層FLAT モード・プログラミング
COMPOSITE モード・プログラミング
インテル® マス・カーネル・ライブラリー (oneMKL) を使用する
インテル® MPI ライブラリーを使用する
高度なトピック
用語275

oneAPI GPU 最適化ガイド

5-16-1

マルチスタック GPU アーキテクチャー

インテル® データセンター GPU マックス・シリーズは、1 から 2 スタックのマルチスタック GPU アーキテクチャーを採用しています。

インテル® Iris® Xe GPU マルチスタック・アーキテクチャー

この図は、1 スタックと 2 スタックのインテル® データセンター GPU マックス・シリーズ製品を示しており、それぞれに専用のリソースがあります。
••
276

ベクトルエンジン (VE): 計算ユニットはスタックに属します。
高帯域幅メモリー (HBM): HBM はスタックに直接接続されます。

oneAPI GPU 最適化ガイド•

レベル 2 キャッシュ (L2): レベル 2 キャッシュはスタックに属します。

一般的なアプリケーションでは、マルチスタック GPU は単一の GPU デバイスとして認識されます。アプリケーション
では、GPU が小さなスタックで内部構成されていることを意識しません。これにより、プログラミング・モデルは簡素化され、コードを変更することなく既存のアプリケーションを実行できます。インテル® GPU ドライバー、SYCL*、および OpenMP* の並列言語ランタイムが、連携してスタック間にワークロードを自動的にディスパッチします。
スタックは、HBM を介してスタック間の効率良い通信を可能にする高速インターコネクトで接続されています。次の操作が考えらえます。
•

任意のスタックが、同じ GPU カードの任意の HBM メモリーに対し読み取り書き込みが可能
例えば、スタック 0 はスタック 1 のローカル HBM メモリーを読み取ることができます。この場合、スタック 0 とス
タック 1 間のインターコネクトが通信に使用されます。

•

各スタックは独立したエンティティー
スタックは単独でワークロードを実行できます。

スタックのローカル HBM アクセスはスタック間相互接続を必要としないため、クロススタック HBM アクセスよりも効率良く低レイテンシーであり、スタック間の帯域幅消費が少なくなります。熟練者は、メモリーの局所性を活用してより高いパフォーマンスを実現できます。
各スタッ クのデフォルトでは、 そのスタッ ク上のすべてのハードウェア 計算リソースを含む単一の Compute
Command Streamer (CCS) を使用します。ほとんどのアプリケーションはこのモードで正常に動作します。
環境変数 ZEX_NUMBER_OF_CCS を使用して、各スタックを 2 CCS モードまたは 4 CCS モード構成に静的に分割し、各 CCS をカーネルをオフロードできるエンティティーとして扱うこともできます。詳細については、「高度なトピッ
ク」を参照してください。

5-16-1-1

GPU メモリーシステム

汎用エンジンのメモリーは、図に示すようにホスト側のメモリーとデバイス側のメモリーに分離され、統合共有メモ
リー (USM) を使用してそれらの間でオブジェクトを移動します。各アドレスは一意のバンクにハッシュされます。シーケンシャル、ストライド、ランダムアドレスの一様分布の近似。
•

Xe コア と同じバンク数では全帯域幅 (キューイングの密集、ホットスポット、バンクの密集により減少)。

•

仮想アドレス変換の TLB ミスにより、メモリー・レイテンシーが増加します。さらに多くのメモリーアクセスとキャッシュの排出を招く可能性があります。

•

GPU キャッシュのミス率が高い場合、アクセスが過度に分散されていると GPU メモリー・コントローラーの効率が低下することがあります。

•

圧縮ユニットは隣接するキャッシュラインを圧縮します。高速クリアサーフェスのリード/ライトもサポートします。
帯域幅を改善できますが、レイテンシーも増加します。

277

oneAPI GPU 最適化ガイド
GPU メモリーシステム

VE で測定された GPU メモリーアクセス:•••

ファブリック帯域幅の持続はピーク時の 90% 程度。
GPU キャッシュヒットは、最大 150 サイクル、キャッシュミスは最大 300 サイクル。TLB ミスは 50～100 サイ
クルを加算。
キャッシュラインへの書き込み後に同じ GPU キャッシュラインを読み取ると、最大 30 サイクルが追加されます。

異なるスタック上のデバイスメモリーにアクセスするスタックは、スタック間の新しい GAM-to-GAM 高帯域幅イン
ターフェイス (GT Link) を利用します。このインターフェイスの帯域幅は、任意の単一スタックのデバイス・メモリー・サ
ブシステムで処理できるメモリー帯域幅と一致します。
VE スレッドのロード/ストア/アトミック
VE スレッドは、メッセージをデータポートに送信してメモリーアクセスを行います。ロード命令はアドレスを送信してデータを受信し、ストア命令はアドレスとデータを送信します。Xe コア のすべての VE は、以下の図のように 1 つのメ
モリーロード/ストア・データポートを共有します。
•••

Xe コア 内部: サイクルごとに ~128 から 256 バイト
Xe コア 外部: サイクルごとに ~64 バイト
読み取り帯域幅が、書き込み帯域幅よりも高い場合があります

VE スレッドとメモリーアクセス

278

oneAPI GPU 最適化ガイド
サイクルごとに新しいメモリーアクセスを開始でき、32 ビットの SIMD16 送信操作は、6 サイクルとメモリー・レイテ
ンシー (12 サイクルとメモリー・レイテンシーで完了する 4 要素のベクトル) で完了します。メモリー帯域幅を最小化するため独立したアドレスがマージされます。メモリー・レイテンシーに注意する必要があります。
メモリー・レイテンシーの表
アクセスタイプ
共有ローカルメモリー
Xe コア データ・キャッシュ・ヒット
GPU キャッシュヒット
GPU キャッシュミス

レイテンシー~30 サイクル~50 サイクル~150 - ~200 サイクル
~300 - ~500 サイクル

すべてのロードとストアは緩和された順序付け (ISO C11 メモリーモデルのリード/ライト) であり、同じスレッドからの同じアドレスに対して順序付けが行われます。同じスレッド内の異なるアドレスは順不同で完了する可能性があり、
リード/ライトの順番はスレッド、VE、Xe コア 間で維持されないため、開発者はアトミックやフェンス操作によって順番を保証する必要があります。
アトミック操作には、メモリー位置からの読み取りと書き込みの両方が含まれる場合があります。アトミック操作は、順序付けされていないアクセスビュー、またはスレッドグループの共有メモリーのいずれかにのみ適用されます。
スレッドがメモリーアドレスに対しアトミック操作を発行すると、アトミックリードとライトの間に、外部から同じアドレスへの書き込みが発生しないことが保証されます。
異なるスレッドからの複数のアトミック操作が同じアドレスである場合、操作は未定義の順番でシリアル化されます。
このシリアル化は、同じアドレスへの L2 シリアル化の規則によって発生します。アトミック操作は、メモリーフェンス
またはスレッドフェンスとは異なります。プログラマーやコンパイラーがフェンスを適切に使用しない場合、すべてのスレッドが任意のメモリー操作の結果を同時に取得することや、ほかのメモリーアドレスの更新を特定の順番で行う
ことは保証されません。ただし、アトミック操作は、常にグローバルレベルで記述され (共有ローカルメモリーを除く)、
アトミック操作が完了すると、結果はすべてのスレッドグループで参照できます。Gen7 以降の世代ごとにアトミック
操作の機能とパフォーマンスは向上しています。
次の SYCL* コードの例では、work-item ごとに 1024 の同一アドレスに対するアトミック操作が実行されます。
各 work-item は異なる (一意の) アドレスを使用し、コンパイラーは VE スレッドごとに SIMD32 カーネルを生成します。これにより、2 つのキャッシュラインで 2 つの SIMD16 アトミック操作が実行され、コンパイラーはループを最
大 8 回アンロールして、レジスター依存関係によるストールを減らすことができます。
1. #include <CL/sycl.hpp>
2. #include <chrono>
3. #include <iostream>
4. #include <string>
5. #include <unistd.h>
6. #include <vector>
7.
8. #ifndef SCALE
9. #define SCALE 1
10. #endif
11.
12. #define N
1024*SCALE
13. #define SG_SIZE 32
14.
15. // 繰り返し回数
16. constexpr int repetitions = 16;
17. constexpr int warm_up_token = -1;
18.
19. static auto exception_handler = [](sycl::exception_list eList) {20.
for (std::exception_ptr const &e : eList) {
21.
try {
22.
std::rethrow_exception(e);279oneAPI GPU 最適化ガイド
23.
} catch (std::exception const &e) {24.
std::cout << "Failure" << std::endl;
25.
std::terminate();
26.
}
27.
}
28. };
29.
30. class Timer {31. public:
32.
Timer() : start_(std::chrono::steady_clock::now()) {}33.
34.
double Elapsed() {35.
auto now = std::chrono::steady_clock::now();36.
return std::chrono::duration_cast<Duration>(now - start_).count();
37.
}
38.
39. private:
40.
using Duration = std::chrono::duration<double>;41.
std::chrono::steady_clock::time_point start_;
42. };
43.
44. #ifdef FLUSH_CACHE
45. void flush_cache(sycl::queue &q, sycl::buffer<int> &flush_buf) {46.
auto flush_size = flush_buf.get_size()/sizeof(int);
47.
auto ev = q.submit([&](auto &h) {48.
sycl::accessor flush_acc(flush_buf, h, sycl::write_only, sycl::noinit);
49.
h.parallel_for(flush_size, [=](auto index) { flush_acc[index] = 1; });
50.
});
51.
ev.wait_and_throw();
52. }53. #endif
54.
55. void atomicLatencyTest(sycl::queue &q, sycl::buffer<int> inbuf,
56.
sycl::buffer<int> flush_buf, int &res, int iter) {57.
const size_t data_size = inbuf.get_size()/sizeof(int);
58.
59.
sycl::buffer<int> sum_buf(&res, 1);
60.
61.
double elapsed = 0;62.
63.
for (int k = warm_up_token; k < iter; k++) {64. #ifdef FLUSH_CACHE
65.
flush_cache(q, flush_buf);
66. #endif
67.
68.
Timer timer;
69.
70.
q.submit([&](auto &h) {71.
sycl::accessor buf_acc(inbuf, h, sycl::write_only, sycl::noinit);
72.
73.
h.parallel_for(sycl::nd_range<1>(sycl::range<>{N}, sycl::range<>{SG_SIZE}),[=](sycl::nd_item<1> item)
74.
[[intel::reqd_sub_group_size(SG_SIZE)]] {75.
int i = item.get_global_id(0);76.
for (int ii = 0; ii < 1024; ++ii) {77.
auto v =78.
#ifdef ATOMIC_RELAXED
79.
sycl::ONEAPI::atomic_ref<int, sycl::ONEAPI::memory_order::relaxed,
80.
sycl::ONEAPI::memory_scope::device,81.
sycl::access::address_space::global_space>(buf_acc[i]);
82.
#else
83.
sycl::ONEAPI::atomic_ref<int, sycl::ONEAPI::memory_order::acq_rel,84.
sycl::ONEAPI::memory_scope::device,85.
sycl::access::address_space::global_space>(buf_acc[i]);86.
#endif87.
v.fetch_add(1);280

oneAPI GPU 最適化ガイド
88.
}
89.
});
90.
});
91.
q.wait();92.
elapsed += (iter == warm_up_token) ? 0 : timer.Elapsed();
93.
}
94.
std::cout << "SUCCESS: Time atomicLatency = " << elapsed << "s" << std::endl;
95. }
96.
97. int main(int argc, char *argv[]) {98.
sycl::queue q{sycl::gpu_selector{}, exception_handler};99.
std::cout << q.get_device().get_info<sycl::info::device::name>() << std::endl;
100.
101. std::vector<int> data(N);102. std::vector<int> extra(N);103.
104. for (size_t i = 0; i < N ; ++i) {105.
data[i] = 1;
106.
extra[i] = 1;
107. }
108. int res=0;109.
110. const sycl::property_list props = {sycl::property::buffer::use_host_ptr()};111. sycl::buffer<int> buf(data.data(), data.size(), props);
112. sycl::buffer<int> flush_buf(extra.data(), extra.size(), props);
113. atomicLatencyTest(q, buf, flush_buf, res, 16);114. }

アトミック操作を使用する実際のワークロードでは、最適な帯域幅を達成するアトミック操作を選択するには、メモ
リーアクセスの動作とデータセットのサイズを理解する必要があります。

5-16-2デバイス階層マルチスタック GPU カードを単一のルートデバイスとすることも、各スタックをルートデバイスとすることもできます。
これは、環境変数 ZE_FLAT_DEVICE_HIERARCHY で制御できます。ZE_FLAT_DEVICE_HIERARCHY に設定可能
な値は、FLAT、COMPOSITE、または COMBINED です。
このガイドでは、FLAT モードと COMPOSITE モードに焦点を当てています。
GPU カードごとに 1 つのスタックがあるシステムでは、FLAT モードと COMPOSITE モードは同じであることに注意
してください。

5-16-2-1

ZE_FLAT_DEVICE_HIERARCHY=FLAT (デフォルト)

ZE_FLAT_DEVICE_HIERARCHY が設定されていない場合、FLAT モードがデフォルトモードになります。FLAT モー
ドでは、各スタックがルートデバイスとなります。FLAT モードを使用することをお勧めします。FLAT モードは、ほとんどのアプリケーションで適切に機能します。
FLAT モードでは、ドライバーと言語ランタイムは、各スタックを他のすべてのスタックとは独立してプログラムできる
ルートデバイスとして公開するツールを提供します。
FLAT モードでは、明示的なスケーリングを使用してオフロードが行われます。
単一または複数の GPU カードシステムでは、ユーザーはすべての GPU カードのすべてのスタックを FLAT モードで使用し、すべてのスタック (デバイス) に同時にオフロードできます。

281

oneAPI GPU 最適化ガイド
OpenMP* では、ターゲット構造の device 句を使用して、カーネルをオフロードするスタック (デバイス) を指定できます。
SYCL* では、platform::get_devices() を呼び出して、公開されているスタック (デバイス) を取得できます。
FLAT モードの詳細については、「FLAT モード・プログラミング」セクションを参照してください。

5-16-2-2

ZE_FLAT_DEVICE_HIERARCHY=COMPOSITECOMPOSITE モードでは、各 GPU カードはルートデバイスとして公開されます。カードに複数のスタックが含まれている場合、GPU カード上のスタックはサブデバイスとして公開されます。
COMPOSITE モードでは、明示的または暗黙的なスケーリングを使用してオフロードを実行できます。
以前の GPU ドライバーでは、デフォルトは COMPOSITE モードと暗黙的なスケーリングでした。現在、デフォルトは
FLAT モードです。
COMPOSITE モードでの明示的なスケーリング:
COMPOSITE モードでは、ドライバーと言語ランタイムは、各 GPU カードをルートデバイスとして公開し、スタックを個別にプログラムできるサブデバイスとして公開するツールを提供します。
OpenMP* では、ターゲット構造の device 句と subdevice 句を使用して、カーネルをオフロードするスタック (サブデバイス) を指定できます (subdevice 句は OpenMP* のインテル拡張であることに注意してください)。
SYCL* では、device::create_sub_devices() を呼び出して、各カードデバイス上のサブデバイスまたはスタックを取得できます。
COMPOSITE モードでの明示的なスケーリングの詳細については、「明示的なスケーリング」を参照してください。
COMPOSITE モードでの暗黙的なスケーリング:
COMPOSITE モードでは、プログラムがカード全体のデバイスにオフロードされる場合、デフォルトでは、ドライバーと言語ランタイムがワークの分散とマルチスタック・メモリーの配置を担当します。
明示的なスケーリングを使用することを推奨します。ただし、メモリー要件が単一のスタックで使用可能なメモリーよりも大きい場合は、暗黙的なスケーリングを使用できます。
COMPOSITE モードでの暗黙的なスケーリングの詳細については、「暗黙的なスケーリング」を参照してください。

5-16-2-3

MPI に関する考慮事項

MPI アプリケーションでは、各 MPI ランクを GPU カードまたは GPU スタック上で実行するように構成できます。各
ランクは、割り当てられた GPU カードまたはスタックを使用するために OpenMP* と SYCL* を使用できます。

282

oneAPI GPU 最適化ガイド
以下の表は、MPI + OpenMP* ハイブリッド・アプリケーションの一般的なデバイス構成を示しています。
MPI + OpenMP の構成 (各 2 スタックの N 枚のカード)
FLAT または公開される

MPI ランクの割り

OpenMP* デバ

暗黙のスケーリ

COMPOSITE

デバイス

当て

イスビュー

ング

推奨

FLAT

スタック

スタックあたり 1 ラデバイス 0 とし

いいえはい

ンク、合計 2*N ランて 1 スタック
いいえ

熟練ユーザー向ク
COMPOSITE

カード

スタックあたり 1 ラデバイス 0 としンク、合計 2*N ランて 1 スタック

けク
COMPOSITECOMPOSITE

カード
カード

カードあたり 1 ラン

デバイス 0 と 1

ク、合計 N ランク

して 2 スタック

カードあたり 1 ラン

デバイス 0 としク、合計 N ランクて 1 カード

いいえ

はい

はい

シングルスタッ
クメモリーが不十分な場合

5-16-2-4

システムおよびデバッグ情報を取得

システムとデバイスに関する情報を取得するには、次の 2 つのスキームを使用できます。


アプリケーションを実行する前に、コマンドラインで sycl-ls コマンドを実行して、プラットフォームで使用できるデバイスを確認することをお勧めします。この情報は、パフォーマンス測定を行うときに特に役立ちます。
sycl-ls は、すべてのバックエンドで認識または管理されているデバイスを表示することに注意してください。
たとえば、合計 2 つのスタックを持つ単一の GPU カードを備えたシステムで実行している場合、sycl-ls は、Level Zero または OpenCL バックエンドのいずれかによって管理されている 2 つのデバイス (2 つのスタックに対応) があることを示します。
$ sycl-ls[level_zero:gpu][level_zero:0] ... Intel(R) Data Center GPU Max 1550 1.3[level_zero:gpu][level_zero:1] ... Intel(R) Data Center GPU Max 1550 1.3[opencl:gpu][opencl:0] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO[opencl:gpu][opencl:1] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO

環境変数 LIBOMPTARGET_DEBUG を 1 に設定すると、ランタイムはどのデバイスが見つかり使用されたかを示すデバッグ情報を表示します。LIBOMPTARGET_DEBUG は OpenMP* 固有であることに注意してください (SYCL* には適用されません)。FLAT モードの例と OpenMP* セクションの例を参照してください。

5-16-2-5

デバイスの認識を制御する環境変数

次の環境変数を使用して、アプリケーションに公開されるハードウェアまたはデバイスを制御できます。


ZE_FLAT_DEVICE_HIERARCHY=FLAT または COMPOSITE (デフォルトは FLAT)。レベル 0 仕様ドキュメントのデバイス階層 (英語) を参照してください。
ONEAPI_DEVICE_SELECTOR。この環境変数は OpenMP* 固有です (SYCL* には適用されません)。環境変数
は、アプリケーションに公開されるハードウェアを制御します。詳細については、oneAPI DPC++ コンパイラー・
ドキュメントの ONEAPI_DEVICE_SELECTOR (英語) を参照してください。
ZE_AFFINITY_MASK。この環境変数は、レベル 0 ユーザー・モード・ドライバー (UMD) によって公開されるハードウェアを制御します。詳細については、レベル 0 仕様ドキュメントの「Affinity Mask」 (英語) を参照してください。
283

oneAPI GPU 最適化ガイド
LIBOMPTARGET_DEVICES=DEVICE ま た は SUBDEVICE ま た は SUBSUBDEVICE 。 こ の 環 境 変 数 はOpenMP* 固有です (SYCL* には適用されません)。OpenMP* 「デバイス」 を GPU カード (デバイス)、スタック(サブデバイス)、または Compute Command Streamer (サブサブデバイス) にマップするために使用できます。
oneAPI GPU 最適化ガイドの「OpenMP* アプリケーションのコンパイルと実行」を参照してください。
ZEX_NUMBER_OF_CCS。高度なトピックのセクションの例を参照してください。



5-16-2-6

参考資料

1. レベル 0 仕様ドキュメントのデバイス階層 (英語)
2. レベル 0 仕様ドキュメントの Affinity Mask (英語)
3. oneAPI DPC++ コンパイラー・ドキュメントの ONEAPI_DEVICE_SELECTOR (英語)
4. oneAPI GPU 最適化ガイドの OpenMP* アプリケーションのコンパイルと実行

5-16-3

FLAT モード・プログラミング

前述のとおり、FLAT モードはインテル® データセンター GPU マックス・シリーズのデフォルトモードです。FLAT モー
ドでは、各スタックがルートデバイスとして公開されます。このセクションでは、FLAT モードでのオフロードを行う
SYCL* および OpenMP* の例を示します。
FLAT モードのメモリー
各スタックには独自のメモリーがあります。スタックにオフロードされたカーネルはそのスタック上で実行され、そのスタックに割り当てられたメモリーを使用します。
スタック上で実行されているカーネルは、同じ GPU カードの他のスタック上のメモリーにアクセスできます。ただし、実行されているスタック以外のスタック上のメモリーにアクセスすると、速度が低下します。
•

FLAT モードの例 - SYCL*•FLAT モードの例 – OpenMP*

5-16-3-1

FLAT モードの例 - SYCL*

このセクションでは、単純なベクトル加算の例を使用して、FLAT モードで複数のデバイスを使用してパフォーマンス
をスケーリングする方法を示します。
単一デバイス （スタック） へオフロード
1 つのデバイスまたはスタックを使用して 2 つのベクトルを加算する例。
float *da;float *db;
float *dc;da = (float *)sycl::malloc_device<float>(gsize, q);
db = (float *)sycl::malloc_device<float>(gsize, q);dc = (float *)sycl::malloc_device<float>(gsize, q);q.memcpy(da, ha, gsize);q.memcpy(db, hb, gsize);q.wait();284

oneAPI GPU 最適化ガイド

std::cout << "Offloading work to 1 device" << std::endl;
for (int i = 0; i < 16; i ++) {q.parallel_for(sycl::nd_range<1>(gsize, 1024),[=](auto idx) {int ind = idx.get_global_id();dc[ind] = da[ind] + db[ind];});}q.wait();std::cout << "Offloaded work completed" << std::endl;
q.memcpy(hc, dc, gsize);

5-16-3-2

FLAT モードの例 - OpenMP*

前述のように、FLAT モードでは、スタックはデバイスとして公開されます。
単一デバイス（スタック）へオフロード
このスキームでは、デフォルトのルートデバイス (デバイス 0) がオフロードに使用されます。以下のコード例を参照してください。
1.
2.
3.
4.
5.
6.

int device_id = omp_get_default_device();#pragma omp target teams distribute parallel for device(device_id) map(...)
for (int i = 0, i < N; i++) {...
}複数のデバイス （スタック） へオフロード
このスキームでは、コードが実行される複数のルートデバイス (スタック) があり、スタックは 1 つ以上の GPU カードに属している場合があります。以下のコード例を参照してください。
1. int num_devices = omp_get_num_devices();2.
3. #pragma omp parallel for
4. for (int device_id = 0; device_id < num_devices; device_id++) {5.
6.
#pragma omp target teams distribute parallel for device(device_id) map(...)
7.
for (int i = lb(device_id); I < ub(device_id); i++) {
8.
...
9.
}
10. }

以下に、FLAT モードで複数のデバイス (スタック) にオフロードする完全な OpenMP* プログラムを示します。

5-16-3-3

OpenMP* の例

次のプログラム flat_openmp_01.cpp では、配列 A がデバイス上で初期化されます。まず、使用可能なデバイス(スタック) の数を決定し、次にデバイス (スタック) を使用して配列のチャンクを初期化します。target プラグマのOpenMP* device 句は、特定のチャンクに使用するスタックを指定するために使用されます (device 句が指定されていない場合、コードはスタック 0 で実行されます)。

285

oneAPI GPU 最適化ガイド
omp_get_num_devices() は、使用可能なデバイス (スタック) の合計数を返します。たとえば、それぞれ 2 つのスタックを持つ 4 枚のカードのシステムでは、ルーチンは 8 を返します。
1. #include <stdlib.h>
2. #include <stdio.h>
3. #include <omp.h>
4.
5. #define SIZE 320
6.
7. int num_devices = omp_get_num_devices();
8. int chunksize = SIZE/num_devices;
9.
10. int main(void)
11. {12. int *A;
13. A = new int[sizeof(int) * SIZE];
14.
15. printf ("num_devices = %d\n", num_devices);
16.
17. for (int i = 0; i < SIZE; i++)18.
A[i] = -9;
19.
20. #pragma omp parallel for21. for (int id = 0; id < num_devices; id++) {22.
#pragma omp target teams distribute parallel for device(id) \23.
map(tofrom: A[id * chunksize : chunksize])24.
for (int i = id * chunksize; i < (id + 1) * chunksize; i++) {25.
A[i] = i;
26.
}
27. }
28.
29. for (int i = 0; i < SIZE; i++)30.
if (A[i] != i)31.
printf ("Error in: %d\n", A[i]);
32.
else
33.
printf ("%d\n", A[i]);34. }コンパイルコマンド:$ icpx -fiopenmp -fopenmp-targets=spir64 flat_openmp_01.cpp

実行コマンド:$ OMP_TARGET_OFFLOAD=MANDATORY ./a.out

注:
OMP_TARGET_OFFLOAD=MANDATORY は、ターゲット領域が GPU 上で実行されることを確認するために使用されます。GPU が見つからない場合、プログラムは失敗します。
FLAT モードがデフォルトであるため、実行コマンドで ZE_FLAT_DEVICE_HIERARCHY=FLAT を指定する必
要はありません。



単一の GPU カード (合計 2 つのスタック) を搭載したシステムで実行する場合:
sycl-ls は、2 つのデバイス (2 つのスタックに対応) があることを示します。
$ sycl-ls[level_zero:gpu][level_zero:0] ... Intel(R) Data Center GPU Max 1550 1.3[level_zero:gpu][level_zero:1] ... Intel(R) Data Center GPU Max 1550 1.3[opencl:gpu][opencl:0] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO

286

oneAPI GPU 最適化ガイド[opencl:gpu][opencl:1] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO

libomptarget.so デバッグ情報を取得するには、実行コマンドに LIBOMPTARGET_DEBUG=1 を追加します。
$ OMP_TARGET_OFFLOAD=MANDATORY LIBOMPTARGET_DEBUG=1 ./a.out >& libomptarget_debug.log

libomptarget_debug.log には次のような出力が含まれ、2 つのデバイス (2 つのスタックに対応) が見つかったことが示されています。
Target LEVEL_ZERO RTL --> Found a GPU device, Name = Intel(R) Data Center GPU Max 1550
Target LEVEL_ZERO RTL --> Found 2 root devices, 2 total devices.
Target LEVEL_ZERO RTL --> List of devices (DeviceID[.SubID[.CCSID]])
Target LEVEL_ZERO RTL --> -- 0
Target LEVEL_ZERO RTL --> -- 1

4 枚の GPU カード (合計 8 スタック) を搭載したシステムで実行
sycl-ls は、8 つのデバイス (8 つのスタックに対応) があることを示しています。
$ sycl-ls[level_zero:gpu][level_zero:0] ... Intel(R) Data Center GPU Max 1550 1.3[level_zero:gpu][level_zero:1] ... Intel(R) Data Center GPU Max 1550 1.3[level_zero:gpu][level_zero:2] ... Intel(R) Data Center GPU Max 1550 1.3[level_zero:gpu][level_zero:3] ... Intel(R) Data Center GPU Max 1550 1.3[level_zero:gpu][level_zero:4] ... Intel(R) Data Center GPU Max 1550 1.3[level_zero:gpu][level_zero:5] ... Intel(R) Data Center GPU Max 1550 1.3[level_zero:gpu][level_zero:6] ... Intel(R) Data Center GPU Max 1550 1.3[level_zero:gpu][level_zero:7] ... Intel(R) Data Center GPU Max 1550 1.3[opencl:gpu][opencl:0] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO[opencl:gpu][opencl:1] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO[opencl:gpu][opencl:2] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO[opencl:gpu][opencl:3] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO[opencl:gpu][opencl:4] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO[opencl:gpu][opencl:5] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO[opencl:gpu][opencl:6] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO[opencl:gpu][opencl:7] ... Intel(R) Data Center GPU Max 1550 OpenCL 3.0 NEO

libomptarget.so デバッグ情報を取得するには、実行コマンドに LIBOMPTARGET_DEBUG=1 を追加します。
$ OMP_TARGET_OFFLOAD=MANDATORY LIBOMPTARGET_DEBUG=1 ./a.out >& libomptarget_debug.log

libomptarget_debug.log には次のような出力が含まれ、8 つのデバイス (8 つのスタックに対応) が見つかったことが示されています。
Target LEVEL_ZERO RTL --> Found a GPU device, Name = Intel(R) Data Center GPU Max 1550
Target LEVEL_ZERO RTL --> Found 8 root devices, 8 total devices.
Target LEVEL_ZERO RTL --> List of devices (DeviceID[.SubID[.CCSID]])
Target LEVEL_ZERO RTL --> -- 0
Target LEVEL_ZERO RTL --> -- 1
Target LEVEL_ZERO RTL --> -- 2
Target LEVEL_ZERO RTL --> -- 3
Target LEVEL_ZERO RTL --> -- 4
Target LEVEL_ZERO RTL --> -- 5
Target LEVEL_ZERO RTL --> -- 6
Target LEVEL_ZERO RTL --> -- 7287

oneAPI GPU 最適化ガイド

5-16-4COMPOSITE モード・プログラミング

前述のように、COMPOSITE モードでは、各 GPU カードはルートデバイスとして公開されます。カードに複数のスタックが含まれている場合、GPU カード上のスタックはサブデバイスとして公開されます。
COMPOSITE モードでは、明示的または暗黙的なスケーリングを使用してオフロードを実行できます。次のセクション
では、暗黙的なスケーリングと明示的なスケーリングについて詳しく説明します。


明示的なスケーリング



暗黙的なスケーリング

5-16-4-1

明示的なスケーリング

明示的なスケーリングでは、プログラマーが work-group の分散とメモリー配置を直接制御します。
••

SYCL* による明示的なスケーリング
OpenMP* による明示的なスケーリング

5-16-4-1-1

SYCL* による明示的なスケーリング

ここでは、SYCL* の明示的なスケーリングの言語 API について説明し、インテル® データセンター GPU マックス・シ
リーズ・プラットフォームでのマルチ GPU とマルチスタックの使い方を説明します。
統合共有メモリー (USM)
ルートデバイスに割り当てられたメモリーは、そのすべてのサブデバイス (スタック) からアクセスできます。つまり、同じルートデバイスにある複数のサブデバイスを持つコンテキストで操作する場合、低速の malloc_host を使用する
代わりに、ルートデバイスで malloc_device を使用できます。malloc_device を使用してホストでデータを参照
する場合、ホストへの明示的なコピーが必要です。3 種類の USM 割り当ての詳細については、統合共有メモリー割り
当てを参照してください。
サブデバイスインテル® データセンター GPU Max 1350 または 1550 には、2 つのスタックがあります。GPU 全体を示すルートデ
バイスは、2 つのサブデバイスに分割でき、各サブデバイスは物理スタックに対応します。
try {vector<device> SubDevices = RootDevice.create_sub_devices<
cl::sycl::info::partition_property::partition_by_affinity_domain>(cl::sycl::info::partition_affinity_domain::numa);}SYCL* では、create_sub_devices を呼び出すたびに、同じサブデバイスが返されます。
インテル® GPU でサポートされるパーティション化は、partition_by_affinity_domain のみであることを忘れないでください。同様に、サポートされているパーティション分割プロパティーは next_partitionable と numa
のみです (どちらも同じことを行います)。
公開されるサブデバイスを制御するには、oneAPI DPC++ コンパイラー・ドキュメントのONEAPI_DEVICE_SELECTOR で説明されている ONEAPI_DEVICE_SELECTOR を使用できます。
288

oneAPI GPU 最適化ガイド
レベル 0 ユーザー・モード・ドライバー (UMD) によって公開されるサブデバイスを制御するには、レベル 0 仕様ドキュ
メントの Affinity Mask で説明されている ZE_AFFINITY_MASK 環境変数を使用できます。
COMPOSITE モードでは、各サブデバイス (スタック) をさらにサブサブデバイス (Compute Command Streamer ま
たは CCS) に分解できます。サブサブデバイスの詳細については、「高度なトピック」セクションを参照してください。
同様に、サポートされるパーティション化のプロパティーは、next_partitionable と numa のみです (これらは同じ動作となります)。CreateMultipleRootDevices=N と NEOReadDebugKeys=1 環境変数を設定して、マルチ
スタックの GPU をエミュレートできます。
コンテキスト
コンテキストは、リソースの分離と共有に使用されます。SYCL* コンテキストは、1 つ以上のデバイスで構成されます。
ルートデバイスとサブデバイスは単一コンテキスト内に配置できますが、それらは同じ SYCL* プラットフォームでなければなりません。複数のデバイスを持つコンテキストに対して作成された SYCL* プログラム (kernel_bundle)
は、コンテキスト内のそれぞれのルートデバイス向けにビルドされます。同じルートデバイスにある複数のサブデバイ
スで構成されるコンテキストの場合、ルートデバイスに対する単一のビルドのみが必要です。
バッファー
SYCL* バッファーもまたコンテキストに対して作成され、レベルゼロ USM 割り当てにマップされます。現在のマッピ
ングを以下に示します。
•

統合デバイスでは、割り当てはホスト上で行われ、ホストとデバイスはコピーなしでアクセスできます。

•

同じルートデバイスのサブデバイス (ルートデバイス自身も含む) のコンテキスト向けのメモリーバッファーは、
ルートデバイスに割り当てられます。したがって、それらはすべてのデバイスから容易にアクセスできます。ホストとの同期は SYCL* RT によって行われ、必要に応じてマップ/アンマップが暗黙のコピーを実行します。

•

異なるルートデバイスのデバイスのコンテキストのメモリーバッファーは、ホストに割り当てられるため、すべてのデバイスからアクセスできます。

キュー
SYCL* キューは、マルチデバイス・コンテキストで常に 1 つのデバイスに接続されることがあります。以下に、パフォー
マンスの高いものから低いものへ、典型的なシナリオを示します。
デバイスの検出
アプリケーションを実行する前に、sycl-ls コマンドを実行して、プラットフォームで利用可能なデバイスを確認してください。これは、パフォーマンスを測定する場合、特に重要です。これにより、実行のフォールバック・パスを確認できます。
ルートデバイス
インテル® GPU は、SYCL* GPU デバイスおよびルートデバイスとして示されます。ルートデバイスを検出するには、
sycl-ls コマンドを使用するのが最も容易です。次に例を示します。
$ sycl-ls[opencl:0] GPU : Intel(R) OpenCL HD Graphics 3.0 [21.35.020776] [level_zero:0] GPU :Intel(R) Level-Zero 1.1 [1.1.20776] [host:0] HOST: SYCL host platform 1.2 [1.2]289

oneAPI GPU 最適化ガイド
sycl-ls は、SYCL* ランタイムで認識されるすべての SYCL* バックエンドのすべてのプラットフォーム・デバイスを表示します。上記の例では、単一の物理 GPU (OpenCL* またはレベルゼロ・バックエンドで管理される) ルートデバイ
スに対応する GPU があります。
「SYCL* ランタイム環境変数」で説明される環境変数 ONEAPI_DEVICE_SELECTOR を使用できます。
$ export ONEAPI_DEVICE_SELECTOR=level_zero:*$ sycl-ls[level_zero:0] GPU : Intel(R) Level-Zero 1.1 [1.1.20776]

システムに複数の GPU が搭載されている場合、それらは異なる複数のルートデバイスとして認識されます。Linux* では、これらは同一 SYCL* プラットフォーム (レベルゼロ・ドライバーを示す) の複数の SYCL* ルートデバイスになります。Windows* では、これらは異なる複数の SYCL* プラットフォーム (レベルゼロ・ドライバーを示す) のルートデバイ
スとして表示されます。
CreateMultipleRootDevices=N と NEOReadDebugKeys=1 環境変数を設定して、複数の GPU カードをエ
ミュレートできます。次に例を示します。
$ export CreateMultipleRootDevices=2$ export NEOReadDebugKeys=1$ export SYCL_DEVICE_FILTER=level_zero$ sycl-ls[level_zero:0] GPU : Intel(R) Level-Zero 1.1 [1.1.20776][level_zero:1] GPU : Intel(R) Level-Zero 1.1 [1.1.20776]

単一サブデバイスに関連付けられたコンテキスト
単一のサブデバイスを含むコンテキストを作成し、キューをそのサブデバイス (スタック) に接続します。このスキーム
では、実行と可視性は単一のサブデバイスに限定され、スタックごとに最高のパフォーマンスが期待できます。コード
例を示します。
try {vector<device> SubDevices = ...;for (auto &D : SubDevices) {// 各キューは個別のコンテキスト内にあり、それらの間でデータが共有されることはありません
auto Q = queue(D);Q.submit([&](handler &cgh) { ... });}}複数のサブデバイスに関連付けられたコンテキスト
同じルートデバイスに複数のサブデバイス (複数スタック) を持つコンテキストを作成します。明示的なスケーリングを効率良く実装するサブデバイスにキューが接続されます。このスキームでは、パフォーマンスを向上するため、ルートデバイスをこのコンテキストに渡すべきではありません。コード例を示します。
try {vector<device> SubDevices = ...;
auto C = context(SubDevices);for (auto &D : SubDevices) {// すべてのキューは同じコンテキストを共有し、データはキュー間で共有できます//
auto Q = queue(C, D);
Q.submit([&](handler &cgh) { ...});}

290

oneAPI GPU 最適化ガイド}

単一のルートデバイスに関連するコンテキスト
単一のルートデバイスを持つコンテキストを作成し、キューをそのルートデバイスに接続します。このスキームでは、ワークは GPU ドライバーによる暗黙のスケーリングによって、すべてのサブデバイス (スタック) に自動的に分散されます。これは、マルチスタック・ハードウェアを有効にする最も容易な方法ですが、特定のスタックをターゲットにする
ことはありません。コード例を示します。
try {// キューはルートデバイスに接続され、ドライバーはサブデバイスに分散します// (存在する場合)
auto D = device(gpu_selector{});auto Q = queue(D);Q.submit([&](handler &cgh) { ...});}

複数のルートデバイスに関連付けられたコンテキスト
複数のルートデバイス (複数のカード) でコンテキストを作成します。このスキームでは、キューが異なるルートデバイ
スに接続される最も自由度の高いコンテキストです。ホストメモリーや明示的なコピーによる低速アクセスと引き換
えに共有の可能性を高めます。コード例を示します。
try {auto P = platform(gpu_selector{});auto RootDevices = P.get_devices();
auto C = context(RootDevices);for (auto &D : RootDevices) {// コンテキストには複数のルートデバイスがあり、データは複数のカード間で共有できます// (明示的なコピーが必要です)
auto Q = queue(C, D);
Q.submit([&](handler &cgh) { ...});}}

明示的に指定するサブデバイスとアルゴリズムに応じて、適切なメモリー割り当て/同期を行います。次のプログラム
は、明示的にサブデバイスを指定する例です (スタックごとに 1 つのキュー)。
1. #include <CL/sycl.hpp>
2. #include <algorithm>
3. #include <cassert>
4. #include <cfloat>
5. #include <iostream>
6. #include <string>
7. using namespace sycl;
8.
9. constexpr int num_runs = 10;
10. constexpr size_t scalar = 3;
11.
12. cl_ulong triad(size_t array_size) {13.
14.
cl_ulong min_time_ns0 = DBL_MAX;15.
cl_ulong min_time_ns1 = DBL_MAX;
16.
17.
device dev = device(gpu_selector_v);
18.
19.
std::vector<device> subdev = {};20.
subdev = dev.create_sub_devices<sycl::info::partition_property::
21.
partition_by_affinity_domain>(sycl::info::partition_affinity_domain::numa);
22.

291

oneAPI GPU 最適化ガイド
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.

queue q[2] = {queue(subdev[0], property::queue::enable_profiling{}),queue(subdev[1], property::queue::enable_profiling{})};
std::cout << "Running on device: " <<q[0].get_device().get_info<info::device::name>() << "\n";std::cout << "Running on device: " <<q[1].get_device().get_info<info::device::name>() << "\n";double *A0 = malloc_shared<double>(array_size/2 * sizeof(double), q[0]);double *B0 = malloc_shared<double>(array_size/2 * sizeof(double), q[0]);double *C0 = malloc_shared<double>(array_size/2 * sizeof(double), q[0]);double *A1 = malloc_shared<double>(array_size/2 * sizeof(double), q[1]);double *B1 = malloc_shared<double>(array_size/2 * sizeof(double), q[1]);double *C1 = malloc_shared<double>(array_size/2 * sizeof(double), q[1]);for ( int i = 0; i < array_size/2; i++) {A0[i]= 1.0; B0[i]= 2.0; C0[i]= 0.0;
A1[i]= 1.0; B1[i]= 2.0; C1[i]= 0.0;}
for (int i = 0; i< num_runs; i++) {auto q0_event = q[0].submit([&](handler& h) {h.parallel_for(array_size/2, [=](id<1> idx) {C0[idx] = A0[idx] + B0[idx] * scalar;});});
auto q1_event = q[1].submit([&](handler& h) {h.parallel_for(array_size/2, [=](id<1> idx) {C1[idx] = A1[idx] + B1[idx] * scalar;});});q[0].wait();q[1].wait();cl_ulong exec_time_ns0 =q0_event.get_profiling_info<info::event_profiling::command_end>() q0_event.get_profiling_info<info::event_profiling::command_start>();std::cout << "Tile-0 Execution time (iteration " << i << ") [sec]: "<< (double)exec_time_ns0 * 1.0E-9 << "\n";min_time_ns0 = std::min(min_time_ns0, exec_time_ns0);
cl_ulong exec_time_ns1 =q1_event.get_profiling_info<info::event_profiling::command_end>() q1_event.get_profiling_info<info::event_profiling::command_start>();std::cout << "Tile-1 Execution time (iteration " << i << ") [sec]: "<< (double)exec_time_ns1 * 1.0E-9 << "\n";min_time_ns1 = std::min(min_time_ns1, exec_time_ns1);}// 正当性のチェック
bool error = false;for ( int i = 0; i < array_size/2; i++) {if ((C0[i] != A0[i] + scalar * B0[i]) || (C1[i] != A1[i] + scalar * B1[i])) {std::cout << "\nResult incorrect (element " << i << " is " << C0[i] <<")!\n";
82.
error = true;
83.
}
84.
}
85.
86.
sycl::free(A0, q[0]);87.
sycl::free(B0, q[0]);88.
sycl::free(C0, q[0]);89.
90.
sycl::free(A1, q[1]);292oneAPI GPU 最適化ガイド
91.
sycl::free(B1, q[1]);92.
sycl::free(C1, q[1]);93.
94.
if (error) return -1;
95.
96.
std::cout << "Results are correct!\n\n";97.
return std::max(min_time_ns0, min_time_ns1);
98. }
99.
100. int main(int argc, char *argv[]) {101.
102. size_t array_size;
103. if (argc > 1 ) {104.
array_size = std::stoi(argv[1]);105. }
106. else {107.
std::cout << "Run as ./<progname> <arraysize in elements>\n";108.
return 1;
109. }
110. std::cout << "Running with stream size of " << array_size
111.
<< " elements (" << (array_size * sizeof(double))/(double)1024/1024 << "MB)\n";112.
113. cl_ulong min_time = triad(array_size);
114.
115. if (min_time == -1) return 1;
116. size_t triad_bytes = 3 * sizeof(double) * array_size;
117. std::cout << "Triad Bytes: " << triad_bytes << "\n";118. std::cout << "Time in sec (fastest run): " << min_time * 1.0E-9 << "\n";119. double triad_bandwidth = 1.0E-09 * triad_bytes/(min_time*1.0E-9);
120. std::cout << "Bandwidth of fastest run in GB/s: " << triad_bandwidth << "\n";121. return 0;122. }

事前 (AOT) コンパイルを行うコマンドを以下に示します。
$ icpx -fsycl -fsycl-targets=spir64_gen -O2 -ffast-math -Xs "-device xehp" explicitsubdevice.cpp -o run.exe

参考資料
1. レベル 0 仕様ドキュメントのアフィニティー・マスク (英語)
2. oneAPI DPC++ コンパイラー・ドキュメントの ONEAPI_DEVICE_SELECTOR (英語)

5-16-4-1-2

OpenMP* による明示的なスケーリング

このセクションでは、COMPOSITE モードでの OpenMP* の明示的なスケーリングについて説明して使用例を示します。
COMPOSITE モードを有効にするには、環境変数 ZE_FLAT_DEVICE_HIERARCHY=COMPOSITE を設定します。
統合共有メモリー (USM)
USM メモリー割り当て向けのインテル拡張機能として、omp_target_alloc_host、omp_target_alloc_device、
omp_target_alloc_shared の 3 つの OpenMP* API が追加されました。
USM をサポートする omp_target_alloc API も追加されています。詳細は、「OpenMP* USM 割り当て API」を参照してください。

293

oneAPI GPU 最適化ガイド
複数のサブデバイスへオフロード
このスキームでは、コードが実行されるサブデバイスが複数あり、キューがサブデバイスに接続されます。これにより、実質的に「明示的なスケーリング」が実現されます。以下のコード例を参照してください。
1. #define DEVKIND 0 // Stack2.
3. int root_id = omp_get_default_device();4.
5. #pragma omp parallel for6. for (int id = 0; id < NUM_SUBDEVICES; ++id) {7.
8. #pragma omp target teams distribute parallel for device(root_id) \9.
subdevice(DEVKIND, id) map(...)
10.
for (int i = lb(id), i < ub(id); i++) {
11.
...
12.
}
13. }

COMPOSITE モードでは、各サブデバイス (スタック) をさらにサブのサブデバイス (Compute Command Streamer
または CCS) に分解できます。サブのサブデバイスの詳細については、「高度なトピック」セクションを参照してください。
単一のルートデバイスへオフロード
このスキームでは、1 つのルートデバイスと、ルートデバイスに接続されたキューがあります。ワークは、GPU ドライ
バーによる「暗黙的なスケーリング」によって、すべてのサブデバイス/スタックに自動的に分散されます。これは、特定のスタックをターゲットにすることなく、マルチスタックの利用を可能にする最も簡単な方法です。以下の例を参照してください。
1.
2.
3.
4.
5.
6.

int root_id = omp_get_default_device();#pragma omp target teams distribute parallel for device(root_id) map(...)
for (int i = 0, i < N; i++) {...
}複数のルートデバイスへオフロード
このスキームでは、複数のルートデバイスがあり、各ルートデバイスは GPU カードです。キューはルートデバイスに接続されているため、共有の可能性が広がりますが、ホストメモリーを介したアクセスが遅くなったり、データが明示的にコピーされたりします。以下のコード例を参照してください。
1. int num_devices = omp_get_num_devices();2.
3. #pragma omp parallel for
4. for (int root_id = 0; root_id < num_devices; root_id++) {5.
6. #pragma omp target teams distribute parallel for device(root_id) map(...)
7.
for (int i = lb(root_id); I < ub(root_id); i++) {
8.
...
9.
}
10. }

294

oneAPI GPU 最適化ガイド
プログラム: COMPOSITE モードでサブデバイス (スタック) にオフロード
選択したデバイスまたは使用するサブデバイス、および使用するアルゴリズムに応じて、適切なメモリー割り当て/同期を必ず行ってください。以下は、COMPOSITE モードで複数のサブデバイス (スタック) にオフロードする完全なOpenMP* プログラムです。
1. #include
2. #include <iostream>
3. #include <omp.h>
4. #include <stdint.h>
5. #ifndef NUM_SUBDEVICES
6. #define NUM_SUBDEVICES 1
7. #endif8.
9. #ifndef DEVKIND
10. #define DEVKIND 0 // Stack
11. #endif
12.
13. template <int num_subdevices> struct mptr {14.
float *p[num_subdevices];15. };
16.
17. int main(int argc, char **argv) {18.
constexpr int SIZE = 8e6;
19.
constexpr int SIMD_SIZE = 32;
20.
constexpr std::size_t TOTAL_SIZE = SIZE * SIMD_SIZE;
21.
constexpr int num_subdevices = NUM_SUBDEVICES;
22.
23.
mptr<num_subdevices> device_ptr_a;24.
mptr<num_subdevices> device_ptr_b;25.
mptr<num_subdevices> device_ptr_c;26.
27.
const int default_device = omp_get_default_device();28.
std::cout << "default_device = " << default_device << std::endl;
29.
30.
for (int sdev = 0; sdev < num_subdevices; ++sdev) {31.
device_ptr_a.p[sdev] =32.
static_cast<float *>(malloc(TOTAL_SIZE * sizeof(float)));33.
device_ptr_b.p[sdev] =34.
static_cast<float *>(malloc(TOTAL_SIZE * sizeof(float)));35.
device_ptr_c.p[sdev] =36.
static_cast<float *>(malloc(TOTAL_SIZE * sizeof(float)));37.
38. #pragma omp target enter data map(alloc
39.
: device_ptr_a.p[sdev] [0:TOTAL_SIZE])
40.
device(default_device) subdevice(DEVKIND, sdev)
41.
42. #pragma omp target enter data map(alloc
43.
: device_ptr_b.p[sdev] [0:TOTAL_SIZE])
44.
device(default_device) subdevice(DEVKIND, sdev)
45.
46. #pragma omp target enter data map(alloc47.
: device_ptr_c.p[sdev] [0:TOTAL_SIZE])
48.
device(default_device) subdevice(DEVKIND, sdev)
49.
} // for (int sdev ...
50.
51.
std::cout << "memory footprint per GPU = "52.
<< 3 * (std::size_t)(TOTAL_SIZE) * sizeof(float) * 1E-9 << " GB"53.
<< std::endl;
54.
55. #pragma omp parallel for56.
for (int sdev = 0; sdev < num_subdevices; ++sdev) {57.
float *a = device_ptr_a.p[sdev];
58.
float *b = device_ptr_b.p[sdev];
59.
60. #pragma omp target teams distribute parallel for device(default_device)61.
subdevice(DEVKIND, sdev)

\
\
\
\
\
\

\

295

oneAPI GPU 最適化ガイド
62.
for (int i = 0; i < TOTAL_SIZE; ++i) {63.
a[i] = i + 0.5;64.
b[i] = i - 0.5;
65.
}
66.
}
67.
68.
const int no_max_rep = 200;69.
double time = 0.0;70.
for (int irep = 0; irep < no_max_rep + 1; ++irep) {71.
if (irep == 1)
72.
time = omp_get_wtime();73.
74. #pragma omp parallel for num_threads(num_subdevices)75.
for (int sdev = 0; sdev < num_subdevices; ++sdev) {76.
float *a = device_ptr_a.p[sdev];
77.
float *b = device_ptr_b.p[sdev];
78.
float *c = device_ptr_c.p[sdev];
79.
80. #pragma omp target teams distribute parallel for device(default_device)81.
subdevice(DEVKIND, sdev)
82.
for (int i = 0; i < TOTAL_SIZE; ++i) {83.
c[i] = a[i] + b[i];
84.
}
85.
}
86.
}
87.
88.
time = omp_get_wtime() - time;
89.
time = time / no_max_rep;
90.
91.
const std::size_t streamed_bytes =92.
3 * (std::size_t)(TOTAL_SIZE)*num_subdevices * sizeof(float);93.
std::cout << "bandwidth = " << (streamed_bytes / time) * 1E-9 << " GB/s"
94.
<< std::endl;
95.
std::cout << "time = " << time << " s" << std::endl;
96.
std::cout.precision(10);97.
98.
for (int sdev = 0; sdev < num_subdevices; ++sdev) {99. #pragma omp target update from(device_ptr_c.p[sdev][:TOTAL_SIZE])
100.
device(default_device) subdevice(DEVKIND, sdev)
101.
std::cout << "-GPU: device id = : " << sdev << std::endl;
102.
std::cout << "target result:" << std::endl;
103.
std::cout << "c[" << 0 << "] = " << device_ptr_c.p[sdev][0] << std::endl;
104.
std::cout << "c[" << SIMD_SIZE - 1
105.
<< "] = " << device_ptr_c.p[sdev][SIMD_SIZE - 1] << std::endl;
106.
std::cout << "c[" << TOTAL_SIZE / 2
107.
<< "] = " << device_ptr_c.p[sdev][TOTAL_SIZE / 2] << std::endl;
108.
std::cout << "c[" << TOTAL_SIZE - 1
109.
<< "] = " << device_ptr_c.p[sdev][TOTAL_SIZE - 1] << std::endl;
110. }
111.
112. for (int sdev = 0; sdev < num_subdevices; ++sdev) {113.
for (int i = 0; i < TOTAL_SIZE; ++i) {114.
assert((int)(device_ptr_c.p[sdev][i]) ==115.
(int)(device_ptr_c.p[sdev][i] +116.
device_ptr_a.p[sdev][i] * device_ptr_b.p[sdev][i]));117.
}
118. }
119.
120. for (int sdev = 0; sdev < num_subdevices; ++sdev) {121. #pragma omp target exit data map(release122.
: device_ptr_a.p[sdev][:TOTAL_SIZE])
123.
device(default_device) subdevice(DEVKIND, sdev)
124. #pragma omp target exit data map(release125.
: device_ptr_b.p[sdev][:TOTAL_SIZE])
126.
device(default_device) subdevice(DEVKIND, sdev)
127. #pragma omp target exit data map(release128.
: device_ptr_a.p[sdev][:TOTAL_SIZE])
129.
device(default_device) subdevice(DEVKIND, sdev)
130. }

296

\

\

\
\
\
\
\
\

oneAPI GPU 最適化ガイド
131. }

コンパイルコマンド:$ icpx -ffp-contract=fast -O2 -ffast-math -DNUM_SUBDEVICES=2 \-fiopenmp -fopenmp-targets=spir64 openmp_explicit_subdevice.cpp

実行コマンド:$ ZE_FLAT_DEVICE_HIERARCHY=COMPOSITE OMP_TARGET_OFFLOAD=MANDATORY ./a.out

この OpenMP* プログラムは、インテル® データセンターGPU マックス・システムで約 2 倍の線形スケーリングを実現します。

5-16-4-1-3

明示的なスケーリングのまとめ

マルチスタック GPU のパフォーマンス・チューニングは、並列化の粒度が細かくなるため、面倒な作業が発生します。
しかし、基本は CPU のパフォーマンス・チューニングと同様です。パフォーマンス・スケーリングを理解するには、次の点に注意してください。
•••

VE 利用効率 - カーネルが異なるスタックの実行リソースをどのように利用するか
データ配置 - 割り当てが異なるスタックの HBM にどのように分散されるか
スレッドデータのアフィニティー - データがどこに配置され、システム内でどのようにアクセスされるか

さらに、アプリケーション開発者が生産性、移植性、およびパフォーマンスのため、スケーリング・スキームを選択する
際に留意すべき重要なプログラミング・モデルの概念があります。
••••

サブデバイス (numa_domains) とサブのサブデバイス (subnuma_domains)
暗黙のおよび明示的なスケーリング
コンテキストとキュー
環境変数とプログラミング言語の API や構造

5-16-4-2

暗黙のスケーリング

ルートデバイスは、スタックとも呼ばれる複数のサブデバイスで構成されます。これらのスタックは、明示的なスタック間の通信を必要としない、ルートデバイスをモノリシック・デバイスとして扱うことを可能にする共有メモリー空間を形成します。この節では、暗黙のスケーリングによるマルチスタック・プログラミングの原則について説明します。暗黙のスケーリングを使用する場合、ルートデバイスのドライバーは、アプリケーション・コードがカーネルを起動する際にすべてのスタックにワークを分散する役割を持ちます。
•
•
•
•

はじめに
ワークのスケジューリングとメモリー分散
STREAM の例
プログラミングの原則

5-16-4-2-1

はじめに暗黙的なスケーリングは、単一の GPU カード内の複数スタックにのみ適用されます。

297

oneAPI GPU 最適化ガイド
COMPOSITE モードでは、プログラムがカード全体のデバイスにオフロードを行う場合、デフォルトでは、ドライバーと言語ランタイムがワークの分散とマルチスタック・メモリーの配置を担当します。
暗黙的な スケ ーリングで は、 アプリケ ーション・ コ ードを変 更する 必要は あ りませ ん。 デバ イス に送信 されたOpenMP*/SYCL* カーネルは、そのデバイス上のすべてのスタックを利用します。同様に、デバイスに割り当てられた
メモリーは、すべてのスタックからアクセス可能になります。ドライバーの動作については、「ワークのスケジュールとメモリーの分散」で説明されています。
暗黙的なスケーリングに関する注意事項:


暗黙的なスケーリングを許可するには、ZE_FLAT_DEVICE_HIERARCHY=COMPOSITE を設定します。
暗黙のスケジュールは、 SYCL*/OpenMP* サブデバイスのセマンティクスと組み合わせてはなりません。
ZE_AFFINITY_MASK で は サ ブ デ バ イ ス 構 文 を 使 用 し な い で く だ さ い 。 つ ま り 、 ル ー ト デ バ イ ス 0(ZE_AFFINITY_MASK=0.0) でスタック 0 を公開する代わりに、ZE_AFFINITY_MASK=0 を介して、またはZE_AFFINITY_MASK の設定を解除して、ルートデバイス全体をドライバーに公開する必要があります。

期待されるパフォーマンス
暗黙のスケーリングでは、すべてのスタックのリソースは一度のカーネル起動で公開されます。2 つのスタックを持つ
ルートデバイスは、カーネルは 2 倍の計算ピーク、2 倍のメモリー帯域幅、および 2 倍のメモリー容量を利用できます。理想的なケースでは、ワークロードのパフォーマンスは 2 倍向上します。ただし、キャッシュサイズとキャッシュ帯
域幅も 2 倍に増加するため、キャッシュ容量にワークロードが収まる場合、線形よりも優れたスケーリングとなることがあります。
各スタックは NUMA ドメインに相当するため、最適な暗黙のスケーリング・パフォーマンスを達成するには、メモリー・アクセス・パターンとメモリー割り当てが重要になります。局所性を意識するワークロードは、クロススタック・メ
モリーのアクセスが自然に最小化されるため、このプログラミング・モデルで最適に機能することが期待されます。計
算集約型のカーネルは NUMA ドメインの影響を受けないため、暗黙のスケーリングを使用して複数のスタックに容易にスケーリングできます。
MPI アプリケーションは、明示的なスケーリングよりも暗黙のスケーリングのほうが効率的です。これは、1 つのラン
クでルートデバイス全体を利用できるため、スタック間の明示的な同期と通信が不要であるためです。暗黙のスケーリ
ングは、1 回のカーネル起動でローカル・メモリー・アクセスとスタック間のメモリーアクセスを自動的にオーバーラッ
プします。
暗黙のスケーリングは、カーネルの実行時間のみを改善します。シリアル・ボトルネックは改善されません。大規模なシリアル・ボトルネックがある場合、暗黙のスケーリングではアプリケーションはスピードアップしません。一般的なシリアル・ボトルネックには以下があります。
•••

高い CPU 使用
カーネル起動レイテンシー
PCIe* 転送

これらは、暗黙のスケーリングによってカーネルの実行時間が短縮されるにつれて顕著になります。スタック 0 のみがホストへ PCIe* 接続されることに注意してください。暗黙のスケーリングが有効化されているインテル® データセン
ター GPU マックス・シリーズでは、カーネル起動のレイテンシーが 3 マイクロ秒程度増加します。

298

oneAPI GPU 最適化ガイド

5-16-4-2-2

ワークのスケーリングとメモリー分散

ルートデバイスのドライバーは、暗黙のスケーリングが使用されると、決定論的なヒューリスティックによって workgroup とメモリーページをすべてのスタックに分散します。このヒューリスティックについては、次の節で説明します。
メモリーの区分
共有割り当てやデバイス割り当てに対応する SYCL*/OpenMP* 割り当ては、すべてのスタックで区分されます。つまり、割り当ては、スタック数のチャンクに分割され、スタック間でラウンドロビン分散されます。次のルートデバイス割
り当てについて考えてみます。
OpenMP*:
int *a = (int*)omp_target_alloc( sizeof(int)*N, device_id );SYCL*:
int *a = sycl::malloc_device<int>(N, q);

2 スタックのルートデバイスでは、前半 (a[0] から a[N/2-1] の要素) はスタック 0 に物理的に割り当てられます。
残りの半分 (a[N/2] から a[N-1] の要素) は、スタック 1 に割り当てられます。将来的には、ユーザー定義のメモリー区分を可能にする API を導入する予定です。
注:
•
•
•

上記のメモリー区分は、ページサイズ単位で適用されます。 ページの割り当てでは、2 ページがスタック 0 に常駐します。
ページサイズ以下の割り当てでは、スタック 0 のみに常駐します。
単一割り当てのメモリープールを使用すると、メモリー区分のロジックが壊れます。オブジェクト・データがすべてのスタックに分散されるように、アプリケーションではオブジェクトごとに 1 つの割り当てを行うことを推奨します。

静的パーティション化
スタックへの work-group のスケジュールは決定論的であり、静的パーティション化と呼ばれます。パーティション化は単純な規則に従います。最も移動の低速な次元はスタック数のチャンクに分割され、スタック間でラウンドロビン
分散されます。ルートデバイスにおける 1 次元のカーネル起動について考えてみます。
OpenMP*:
1.
2.
3.
4.
5.

#pragma omp target teams distribute parallel for simdfor (int i = 0; i < N; ++i){//}SYCL*:
1.
2.
3.

q.parallel_for(N, [=](auto i) {
//
});

299

oneAPI GPU 最適化ガイド
次元は 1 つしかないため、自動的に最も低速な次元になり、ドライバーによってスタック間で分割されます。2 スタッ
クのルートデバイスでは 反復 0 から N/2-1 はスタック 0 にスケジュールされます。残りの反復 N/2 から N-1 は、ス
タック 1 で実行されます。
OpenMP* で collapse 節が使用されると、最も移動の低速な次元は、最も外側のループになります。SYCL* では、最も低速な次元はグローバルレンジの最初の要素です。例えば、3D カーネルの起動について考えてみます。
OpenMP*:
1. #pragma omp target teams distribute parallel for simd collapse(3)2. for (int z = 0; z < nz; ++z)
3. {
4.
for (int y = 0; y < ny; ++y)5.
{
6.
for (int x = 0; x < nx; ++x)
7.
{
8.
//
9.
}
10.
}
11. }

SYCL*:
1.
2.
3.
4.
5.
6.

range<3> global{nz, ny, nx};
range<3> local{1, 1, 16};cgh.parallel_for(nd_range<3>(global, local), [=](nd_item<3> item) {//});最も低速な次元は z であり、スタック間で分割されます。これは、2 スタックのルートデバイスの場合、z=0 から
z=nz/2-1 の反復がスタック 0 で実行されます。残りの反復 z=nz/2 から z=nz-1 は、スタック 1 にスケジュールされます。
最も低速な次元をスタック間で均等に分割できず、5% を超えるインバランスが生じる場合、ドライバーはインバラン
スを解消するため、次の次元を分割します。これは、奇数次元が 19 未満のカーネルにのみ影響します。以下にさまざ
まなカーネル起動の例を示します (ローカルレンジ {1,1,16} を想定)。
スタックへの work-group のパーティション化
nz

ny

nx

512
21
19
18
19

512
512
512
512
19

512
512
512
512
512

パーティションzzyzx

SYCL* の多次元ローカルレンジでは、分割された次元が変更される可能性があります。例えば、ローカルレンジ{2,1,8} を持つグローバルレンジ {38,512,512} の場合、ドライバーは y 次元を分割し、ローカルレンジ {1,1,16} の場合、ドライバーは z 次元を分割します。OpenMP* では、最も内側のループで作成された 1 次元ローカルレンジを持つことが許されるため、静的パーティション化には影響しません。3 レベル以上の collapse 節を持つ OpenMP*
カーネルは、入れ子になったすべての for ループが線形化された 1 次元のカーネルに相当します。この線形化された
ループは、1D カーネルの起動ヒューリスティックに従って分割されます。

300

oneAPI GPU 最適化ガイド
注:••

静的パーティション化は work-group 単位で行われます。つまり、work-group 内のすべての work-item が、同じスタックにスケジュールされるということです。
単一 work-group を持つカーネルはスタック 0 にのみ常駐します。

5-16-4-2-3

STREAM の例

カーネルの例：OpenMP*:
1.
2.
3.
4.
5.
6.
7.

int *a = (int *)omp_target_alloc(sizeof(int) * N, device_id);#pragma omp target teams distribute parallel for simdfor (int I = 0; I < N; ++i){a[i] = i;}SYCL*:
1.
2.
3.
4.
5.

int *a = sycl::malloc_device<int>(N, q);q.parallel_for(N, [=](auto i) {a[i] = i;});

暗黙のスケーリングによって、ローカル・メモリー・アクセスは 100% 保証されます。静的パーティション化とメモリー区分の動作を以下に示します。

次の節では、インテル® データセンター GPU マックス・シリーズで 1D および 3D カーネルを起動して、STREAM ベン
チマークの暗黙のスケーリングのパフォーマンスを示しています。
STREAM
OpenMP* で記述された STREAM ベンチマークについて考えてみます。メインのカーネルは 38-41 行目にあります。
1.
2.
3.
4.

// STREAM のコード:#include <iostream>#include <omp.h>#include <cstdint>

301

oneAPI GPU 最適化ガイド
5.
6. // 以下でコンパイル:
7. // icpx -O2 -fiopenmp -fopenmp-targets=spir64 ./stream.cpp
8.
9. int main()
10. {
11.
constexpr int64_t N = 256 * 1e6;
12.
constexpr int64_t bytes = N * sizeof(int64_t);
13.
14.
int64_t *a = static_cast<int64_t *>(malloc(bytes));15.
int64_t *b = static_cast<int64_t *>(malloc(bytes));16.
int64_t *c = static_cast<int64_t *>(malloc(bytes));
17.
18.
#pragma omp target enter data map(alloc:a[0:N])
19.
#pragma omp target enter data map(alloc:b[0:N])
20.
#pragma omp target enter data map(alloc:c[0:N])
21.
22.
for (int i = 0; i < N; ++i)
23.
{
24.
a[i] = i + 1;
25.
b[i] = i - 1;
26.
}
27.
28.
#pragma omp target update to(a[0:N])
29.
#pragma omp target update to(b[0:N])
30.
31.
const int no_max_rep = 100;32.
double time;33.
for (int irep = 0; irep < no_max_rep + 10; ++irep)
34.
{
35.
if (irep == 10)
36.
time = omp_get_wtime();37.
38.
#pragma omp target teams distribute parallel for simd39.
for (int i = 0; i < N; ++i)
40.
{
41.
c[i] = a[i] + b[i];
42.
}
43.
}
44.
time = omp_get_wtime() - time;
45.
time = time / no_max_rep;
46.
47.
#pragma omp target update from(c[0:N])
48.
49.
for (int i = 0; i < N; ++i)
50.
{
51.
if (c[i] != 2 * i)
52.
{
53.
std::cout << "wrong results!" << std::endl;
54.
exit(1);
55.
}
56.
}
57.
58.
const int64_t streamed_bytes = 3 * N * sizeof(int64_t);
59.
60.
std::cout << "bandwidth = " << (streamed_bytes / time) * 1E-9
61.
<< " GB/s" << std::endl;
62. }ベンチマークは、環境変数 EnableImplicitScaling=1 を設定して、暗黙のスケーリングを有効にすることで、ルートデバイス全体で実行されます。コードを変更する必要はありません。静的パーティション化とメモリー区分のヒューリスティックにより、各スタックがローカルメモリーにのみアクセスすることが保証されます。 2 スタックのインテル® データセンター GPU マックス・シリーズのシステムでは、単一スタックと比較して STEAM は 2 倍のスピードアップを示しました。測定された帯域幅を以下に示します。

302

oneAPI GPU 最適化ガイド
１D カーネル起動の帯域幅を測定
配列サイズ [MB]1 スタック帯域幅 [GB/秒]

暗黙のスケーリング帯域
幅 [GB/秒)]

暗黙のスケーリング 1 スタック
以上のスピードアップ

512
1024
2048

1056
1059
1063

2074
2127
2113

1.96x
2x
1.99x

3D STREAMSTREAM ベンチマークは、OpenMP* の collapse 節を使用して 3D カーネル起動を使用するように変更できます。
ここでは、ドライバーのヒューリスティックにより、3D カーネル起動をスタック間で分割するパフォーマンスを理解することです。カーネルは 52-62 行目にあります。
1. // 3D STREAM のコード
2. #include <iostream>
3. #include <omp.h>
4. #include <cassert>
5.
6. // 以下でコンパイル:
7. // icpx -O2 -fiopenmp -fopenmp-targets=spir64 ./stream_3D.cpp
8.
9. int main(int argc, char **argv)
10. {
11.
const int device_id = omp_get_default_device();12.
const int desired_total_size = 32 * 512 * 16384;
13.
const std::size_t bytes = desired_total_size * sizeof(int64_t);
14.
15.
std::cout << "memory footprint = " << 3 * bytes * 1E-9 << " GB" << std::endl;
16.
17.
int64_t *a = static_cast<int64_t*>(omp_target_alloc_device(bytes, device_id));18.
int64_t *b = static_cast<int64_t*>(omp_target_alloc_device(bytes, device_id));19.
int64_t *c = static_cast<int64_t*>(omp_target_alloc_device(bytes, device_id));20.
21.
const int min = 64;
22.
const int max = 32768;23.
24.
for (int lx = min; lx < max; lx *= 2)
25.
{
26.
for (int ly = min; ly < max; ly *= 2)
27.
{
28.
for (int lz = min; lz < max; lz *= 2)
29.
{
30.
const int total_size = lx * ly * lz;
31.
if (total_size != desired_total_size)
32.
continue;
33.
34.
std::cout << "lx=" << lx << " ly=" << ly << " lz=" << lz << ", ";35.
36.
#pragma omp target teams distribute parallel for simd37.
for (int i = 0; i < total_size; ++i)
38.
{
39.
a[i] = i + 1;
40.
b[i] = i - 1;
41.
c[i] = 0;
42.
}
43.
44.
const int no_max_rep = 40;
45.
const int warmup = 10;
46.
double time;47.
for (int irep = 0; irep < no_max_rep + warmup; ++irep)
48.
{
49.
if (irep == warmup) time = omp_get_wtime();
50.

303

oneAPI GPU 最適化ガイド
51.
#pragma omp target teams distribute parallel for simd collapse(3)52.
for (int iz = 0; iz < lz; ++iz)
53.
{
54.
for (int iy = 0; iy < ly; ++iy)
55.
{
56.
for (int ix = 0; ix < lx; ++ix)
57.
{
58.
const int index = ix + iy * lx + iz * lx * ly;59.
c[index] = a[index] + b[index];
60.
}
61.
}
62.
}
63.
}
64.
time = omp_get_wtime() - time;
65.
time = time / no_max_rep;
66.
67.
const int64_t streamed_bytes = 3 * total_size * sizeof(int64_t);
68.
69.
std::cout << "bandwidth = " << (streamed_bytes / time) * 1E-9
70.
<< " GB/s" << std::endl;
71.
72.
#pragma omp target teams distribute parallel for simd73.
for (int i = 0; i < total_size; ++i)
74.
{
75.
assert(c[i] == 2 * i);
76.
}
77.
}
78.
}
79.
}
80.
81.
omp_target_free(a, device_id);82.
omp_target_free(b, device_id);83.
omp_target_free(c, device_id);84. }最も内側のループのメモリー・アクセス・パターンは、ストライド 1 であることに注目してください。z ループまたは y
ループが最も内側のループである場合、キャッシュラインの利用率が低下する分散ロードとストアが行われるため、パフォーマンスが低下します。2GB アレイサイズの 2 スタックのインテル® データセンター GPU マックス・シリーズのシステムでは、次のパフォーマンスが測定されました。
3D カーネル起動の帯域幅を測定:
nx

ny

nz

64
16834
256

256
64
16834

16834
256
64

1 スタック帯域幅[GB/秒]
1040
1040
1037

暗黙の大規模帯域幅 [GB/秒)]
2100
2077
2079

1 スタック以上の暗黙のスケーリングのスピードアップ
2.01x
1.99x
2x

「静的パーティション化」の項で説明されているように、これらのループ境界では、ドライバーは両方のスタック間で最も低速な次元である z を分割します。これにより、各スタックがローカルメモリーのみにアクセスすることが保証され、単一スタックと比較して 2 倍近い暗黙のスケーリングが可能になります。

5-16-4-2-4

プログラミングの原則

暗黙的なスケーリングで優れたパフォーマンスを達成するには、スタック間のメモリーアクセスを最小限にする必要がありますが、そのすべてを排除する必要はありません。ローカルメモリーと同時にスタック間のメモリーがアクセスされる場合、一定のスタック間のトラフィックは相互接続で処理できます。メモリー帯域幅が制限されるワークロード
では、許容されるスタック間のアクセス量は、ローカルメモリー帯域幅とスタック間の帯域幅の比率によって決定されます (「スタック間のトラフィック」を参照)。

304

oneAPI GPU 最適化ガイド
暗黙のスケーリングを使用するワークロードは、以下の原則に従う必要があります。
•

カーネルには両方のスタックを利用するのに十分な work-item が必要です。
‐ 必要となる work-item の最小数は、<VE 数> * <VE ごとのハードウェア・スレッド数> * <SIMD 幅> になります。
‐ 1024 の VE と SIMD32 の 2 スタックのインテル® データセンター GPU マックス・シリーズでは、最低
262,144 の work-item です。

•

アプリケーション全体のスケーリングを観察するには、デバイス時間が実行時間の大部分でなければなりません。

•

アルゴリズムの局所性により、スタック間のアクセスを最小化します。

•

スタック間の負荷インバランスを避けるため、最も低速な次元を大きくする必要があります。

•

スタック間のメモリーアクセスとローカル・メモリー・アクセスは、インターリーブする必要があります。

•

2D および 3D カーネルの起動でも、最も低速な次元でストライド 1 のメモリーアクセスを回避します。

•

メモリー・アクセス・パターンが時間経過とともに動的に変化する場合、スタック間のメモリーアクセスを最小化するため、n 回の反復ごとにソートステップを実行する必要があります。

•

単一割り当てのメモリープールを使用してはなりません (メモリー区分を参照)。

ほとんどのアプリケーションには、自然に局所性の概念が備わっています。それらのアプリケーションは、スタック間のトラフィックが少ないため、暗黙のスケーリングが適していると考えられます。これを説明するため、ステンシル
カーネルについて考えてみます。ステンシルはブロックに分割可能なグリッド上で動作し、ブロック内のステンシル計
算のほとんどはスタックのローカルデータを使用します。ブロック境界のステンシル操作のみが、別のブロック (別のスタック) のデータを必要とします。スタック間/境界間のアクセス量は、ハローとローカルの体積比によって制限されます。この概念を以下に示します。

スタック間のトラフィック
前述したように、スタック間のトラフィックは最小限にする必要があります。アプリケーションのパフォーマンスに大きく影響しないトラフィック量を求めるには、さまざまな量のスタック間トラフィックで STREAM カーネルを実行し、スタックローカルの STREAM のパフォーマンスと比較します。最悪のケースは 100% のスタック間トラフィックです。
これは、STREAM カーネルのループの順番を反転することで検証できます (「STREAM」を参照)。
1.
2.
3.

#pragma omp target teams distribute parallel for simdfor (int i = N - 1; i <= 0; --i){305

oneAPI GPU 最適化ガイド
4.
5.

c[i] = a[i] + b[i];}スタック 0 の work-group がスタック 1 のメモリーに配置されている配列要素 N-1 から N/2 にアクセスするため、スタック間のメモリー・トラフィックは 100% になります。このカーネルは、スタック間の双方向帯域幅をベンチマー
クします。このアプローチは、以下に示す STREAM によって 0% のスタック間アクセスと 100% のスタック間アクセスを補完するように一般化できます。
1. // スタック間 stream のコード
2. #include <iostream>
3. #include <omp.h>
4.
5. // 以下でコンパイル:
6. // icpx -O2 -fiopenmp -fopenmp-targets=spir64 ./stream_cross_stack.cpp
7. // 以下で実行:
8. // EnableWalkerPartition=1 ZE_AFFINITY_MASK=0 ./a.out
9.
10. template <int cross_stack_fraction>
11. void cross_stack_stream() {12.
13.
constexpr int64_t size = 256*1e6;
14.
constexpr int64_t bytes = size * sizeof(int64_t);
15.
16.
int64_t *a = static_cast<int64_t*>(malloc( bytes ));17.
int64_t *b = static_cast<int64_t*>(malloc( bytes ));18.
int64_t *c = static_cast<int64_t*>(malloc( bytes ));19.
20. #pragma omp target enter data map( alloc:a[0:size] )21. #pragma omp target enter data map( alloc:b[0:size] )22. #pragma omp target enter data map( alloc:c[0:size] )23.
24.
for ( int i = 0; i < size; ++i ) {25.
a[i] = i + 1;26.
b[i] = i - 1;27.
c[i] = 0;28.
}29.
30. #pragma omp target update to( a[0:size] )31. #pragma omp target update to( b[0:size] )32. #pragma omp target update to( c[0:size] )33.
34.
const int num_max_rep = 100;35.
double time;36.
37.
for ( int irep = 0; irep < num_max_rep+10; ++irep ) {38.
if ( irep == 10 ) time = omp_get_wtime();39. #pragma omp target teams distribute parallel for simd40.
for ( int j = 0; j < size; ++j ) {41.
const int cache_line_id = j / 16;42.
int i;
43.
44.
if ( (cache_line_id%cross_stack_fraction) == 0 ) {45.
i = (j+size/2)%size;
46.
}
47.
else {
48.
i = j;
49.
}
50.
c[i] = a[i] + b[i];
51.
}
52.
}
53.
time = omp_get_wtime() - time;
54.
time = time/num_max_rep;
55.
56. #pragma omp target update from( c[0:size] )57.
for ( int i = 0; i < size; ++i ) {306

oneAPI GPU 最適化ガイド58.
if ( c[i] != 2*i ) {59.
std::cout << "wrong results!"<< std::endl;
60.
exit(1);
61.
}
62.
}
63.
64.
const int64_t streamed_bytes = 3 * size * sizeof(int64_t);
65.
std::cout << "cross_stack_percent = " << (1/(double)cross_stack_fraction)*100
66.
<< "%, bandwidth = " << (streamed_bytes/time) * 1E-9 << " GB/s" << std::endl;
67. }
68.
69. int main() {70.
cross_stack_stream< 1>();71.
cross_stack_stream< 2>();72.
cross_stack_stream< 4>();73.
cross_stack_stream< 8>();74.
cross_stack_stream<16>();75.
cross_stack_stream<32>();76. }40－51 行目のカーネルは、配列アクセスを (j+N/2)%N でオフセットすることで、cross_stack_fraction 番目のキャッシュラインごとにスタック間にアクセスします。cross_stack_fraction==1 の場合、100% のスタック間
メモリーアクセスを行います。cross_stack_fraction を 2 倍にすることで、スタック間のトラフィックを半分にできます。このカーネルは、スタック間とローカルメモリーのアクセスが work-group 内でインターリーブされ、ハードウェアの利用率を最大化するように設計されています。2GB アレイサイズの 2 スタックのインテル® データセンター
GPU マックス・シリーズで測定したパフォーマンスを以下に示します。
スタック間アクセスの帯域幅を測定
部分的スタック間の
STREAM 帯域幅 [GB/秒]
355
696
1223
1450
1848
2108

スタック間比率

スタック間アクセスの %

1
2
4
8
16
32

100%
50%
25%
12.5%
6.25%
3.125%

最大ローカル 2 スタック
STREAM 帯域幅の %
17%
33%
58%
69%
87%
99%

上の表から分かるように、アプリケーションはスタック間のトラフィックを全メモリー・トラフィックの 10% 未満に制限して、持続的なメモリー帯域幅の低下を回避する必要があります。スタック間のアクセスが 12.5% の STREAM では、ローカル STREAM ベンチマークの帯域幅の 69% でした。これらの数値から、スタック間のメモリーアクセスがア
プリケーションのカーネル実行に与える影響を算出できます。

5-16-5インテル® oneAPI マス・カーネル・ライブラリー (oneMKL) を使用

SYCL* での インテル® oneAPI マス・カーネル・ライブラリー (oneMKL) によるパフォーマンスのスケーリング
OpenMP* でのインテル® oneAPI マス・カーネル・ライブラリー (oneMKL) によるパフォーマンスのスケーリン
グ

5-16-5-1

SYCL* でのインテル® oneAPI マス・カーネル・ライブラリー (oneMKL) によるパフォーマンスのスケーリング

このセクションでは、マルチ・スタック・プラットフォームでインテル® oneAPI マス・カーネル・ライブラリー (oneMKL)
で明示的なスケーリングを使用する方法を説明します。

307

oneAPI GPU 最適化ガイド
次のコードは、GEMM 計算を 2 つのスタックに分割する完全な例を示します。この例では、{S,D,C,Z}GEMM API の明
示的なスケーリングを示します。
#include "mkl.h"#include "oneapi/mkl/blas.hpp"#include <CL/sycl.hpp>// Helper functions
template <typename fp> fp set_fp_value(fp arg1, fp arg2 = 0.0) { return arg1; }template <typename fp>
std::complex<fp> set_fp_value(std::complex<fp> arg1,
std::complex<fp> arg2 = 0.0) {return std::complex<fp>(arg1.real(), arg2.real());}
template <typename fp>
void init_matrix(fp *M, oneapi::mkl::transpose trans, int64_t m, int64_t n,
int64_t ld) {if (trans == oneapi::mkl::transpose::nontrans) {for (int64_t j = 0; j < n; j++)for (int64_t i = 0; i < m; i++)M[i + j * ld] = fp(i + j);} else {for (int64_t i = 0; i < m; i++)for (int64_t j = 0; j < n; j++)M[j + i * ld] = fp(i + j);}}// GEMM operation is performed as shown below.
// C = alpha * op(A) * op(B) + beta * C
template <typename fp> void run_gemm(const sycl::device &dev) {// Initialize data for GEMM
oneapi::mkl::transpose transA = oneapi::mkl::transpose::nontrans;
oneapi::mkl::transpose transB = oneapi::mkl::transpose::nontrans;// Matrix data sizes
int64_t m = 1024;
int64_t n = 1024;
int64_t k = 1024;// Leading dimensions of matrices
int64_t ldA = 1024;
int64_t ldB = 1024;
int64_t ldC = 1024;// Set scalar fp values
fp alpha = set_fp_value(2.0, -0.5);
fp beta = set_fp_value(3.0, -1.5);// Create sub devices for multi stack device
std::vector<sycl::device> subdev = {};sycl::queue subdev_queue0;
sycl::queue subdev_queue1;
try {using namespace sycl::info;
subdev = dev.create_sub_devices<
partition_property::partition_by_affinity_domain>(partition_affinity_domain::numa);
std::cout << "\tUse " << subdev.size() << " subdevices" << std::endl;} catch (sycl::exception &e) {std::cout << "\tSubdevices not found, use root device" << std::endl;}
int64_t nb_device = subdev.size();308

oneAPI GPU 最適化ガイド// Catch asynchronous exceptions
auto exception_handler = [](sycl::exception_list exceptions) {for (std::exception_ptr const &e : exceptions) {try {std::rethrow_exception(e);} catch (sycl::exception const &e) {std::cout << "Caught asynchronous SYCL exception during GEMM:\n"<< e.what() << std::endl;}}};// Create context and execution queue
subdev.push_back(dev);
sycl::context cxt(subdev);
sycl::queue main_queue(cxt, dev, exception_handler);// If two subdevices are detected, create queue for each subdevice
if (nb_device > 1) {subdev_queue0 = sycl::queue(cxt, subdev[0], exception_handler);subdev_queue1 = sycl::queue(cxt, subdev[1], exception_handler);}// Allocate and initialize arrays for matrices
int64_t sizea;
if (transA == oneapi::mkl::transpose::nontrans)
sizea = ldA * k;else
sizea = ldA * m;
int64_t sizeb = ldB * n;
int64_t sizec = ldC * n;auto A_host = sycl::malloc_host<fp>(sizea, main_queue);auto B_host = sycl::malloc_host<fp>(sizeb, main_queue);auto C_host = sycl::malloc_host<fp>(sizec, main_queue);
init_matrix(A_host, transA, m, k, ldA);
init_matrix(B_host, transB, k, n, ldB);
init_matrix(C_host, oneapi::mkl::transpose::nontrans, m, n, ldC);// Copy A/B/C from host to device/subdevices// When two stacks are detected,// GEMM operation is split between stacks in n direction// All A matrix is copied to both stacks.
// B and C matrices are split between stacks, so only half of B and C are// copied to each stack.
fp *A_dev0, *A_dev1, *B_dev0, *B_dev1, *C_dev0, *C_dev1, *A_dev, *B_dev,*C_dev;
if (nb_device > 1) {A_dev0 = sycl::malloc_device<fp>(sizea, subdev_queue0);
A_dev1 = sycl::malloc_device<fp>(sizea, subdev_queue1);
B_dev0 = sycl::malloc_device<fp>(sizeb / 2, subdev_queue0);
B_dev1 = sycl::malloc_device<fp>(sizeb / 2, subdev_queue1);
C_dev0 = sycl::malloc_device<fp>(sizec / 2, subdev_queue0);
C_dev1 = sycl::malloc_device<fp>(sizec / 2, subdev_queue1);// Entire A matrix is copied to both stacks
subdev_queue0.memcpy(A_dev0, A_host, sizea * sizeof(fp));
subdev_queue1.memcpy(A_dev1, A_host, sizea * sizeof(fp));// Half of B and C are copied to each stack
subdev_queue0.memcpy(B_dev0, B_host, (sizeb / 2) * sizeof(fp));
subdev_queue1.memcpy(B_dev1, B_host + ldB * n / 2,
(sizeb / 2) * sizeof(fp));
subdev_queue0.memcpy(C_dev0, C_host, (sizec / 2) * sizeof(fp));
subdev_queue1.memcpy(C_dev1, C_host + ldC * n / 2,
(sizec / 2) * sizeof(fp));
subdev_queue0.wait();subdev_queue1.wait();309

oneAPI GPU 最適化ガイド} else {A_dev = sycl::malloc_device<fp>(sizea, main_queue);
B_dev = sycl::malloc_device<fp>(sizeb, main_queue);
C_dev = sycl::malloc_device<fp>(sizec, main_queue);
main_queue.memcpy(A_dev, A_host, sizea * sizeof(fp));
main_queue.memcpy(B_dev, B_host, sizeb * sizeof(fp));
main_queue.memcpy(C_dev, C_host, sizec * sizeof(fp));
main_queue.wait();}// Call oneMKL GEMM API// When two stacks are detected, GEMM call is launched on each stacksycl::event gemm_done0;
sycl::event gemm_done1;
std::vector<sycl::event> gemm_dependencies;try {if (nb_device > 1) {// Split B and C for each stack
int64_t n_subdev = n / 2;
gemm_done0 = oneapi::mkl::blas::gemm(
subdev_queue0, transA, transB, m, n_subdev, k, alpha, A_dev0, ldA,
B_dev0, ldB, beta, C_dev0, ldC, gemm_dependencies);
gemm_done1 = oneapi::mkl::blas::gemm(
subdev_queue1, transA, transB, m, n_subdev, k, alpha, A_dev1, ldA,
B_dev1, ldB, beta, C_dev1, ldC, gemm_dependencies);} else {gemm_done0 = oneapi::mkl::blas::gemm(main_queue, transA, transB, m, n, k,alpha, A_dev, ldA, B_dev, ldB, beta,
C_dev, ldC, gemm_dependencies);}} catch (sycl::exception const &e) {std::cout << "\t\tCaught synchronous SYCL exception during GEMM:\n"<< e.what() << std::endl;}// Wait for GEMM calls to finish
gemm_done0.wait();if (nb_device > 1)
gemm_done1.wait();// Copy C from device/subdevices to hostif (nb_device > 1) {subdev_queue0.memcpy(C_host, C_dev0, (sizec / 2) * sizeof(fp));
subdev_queue1.memcpy(C_host + ldC * n / 2, C_dev1,(sizec / 2) * sizeof(fp));
subdev_queue0.wait();subdev_queue1.wait();} else {main_queue.memcpy(C_host, C_dev, sizec * sizeof(fp));
main_queue.wait();}// Clean-up
free(A_host, cxt);
free(B_host, cxt);
free(C_host, cxt);
if (nb_device > 1) {sycl::free(A_dev0, subdev_queue0);
sycl::free(A_dev1, subdev_queue1);
sycl::free(B_dev0, subdev_queue0);
sycl::free(B_dev1, subdev_queue1);
sycl::free(C_dev0, subdev_queue0);
sycl::free(C_dev1, subdev_queue1);} else {sycl::free(A_dev, main_queue);sycl::free(B_dev, main_queue);sycl::free(C_dev, main_queue);}310

oneAPI GPU 最適化ガイド
if (nb_device > 1)
std::cout << "\tGEMM operation is complete on 2 stacks" << std::endl;
else
std::cout << "\tGEMM operation is complete on 1 stack" << std::endl;}// Main entry point for example.
// GEMM example is run on GPU for 4 data types.
int main(int argc, char **argv) {// Create GPU device
sycl::device dev = sycl::device(sycl::gpu_selector_v);
std::cout << "Running with single precision real data type:" << std::endl;
run_gemm<float>(dev);
std::cout << "Running with double precision real data type:" << std::endl;
run_gemm<double>(dev);
std::cout << "Running with single precision complex data type:" << std::endl;
run_gemm<std::complex<float>>(dev);
std::cout << "Running with double precision complex data type:" << std::endl;
run_gemm<std::complex<double>>(dev);
return 0;}

この例では、GEMM の最初のスカラー・パラメーターと次元パラメーターが初期化されます。
// Initialize data for GEMM
oneapi::mkl::transpose transA = oneapi::mkl::transpose::nontrans;
oneapi::mkl::transpose transB = oneapi::mkl::transpose::nontrans;// Matrix data sizes
int64_t m = 1024;
int64_t n = 1024;
int64_t k = 1024;// Leading dimensions of matrices
int64_t ldA = 1024;
int64_t ldB = 1024;
int64_t ldC = 1024;// Set scalar fp values
fp alpha = set_fp_value(2.0, -0.5);
fp beta = set_fp_value(3.0, -1.5);

次に、利用可能であればルートデバイスからサブデバイスが作成され、SYCL* ベクイトルに保存されます。プラット
フォームにサブデバイスがない場合 (シングル・スタック・デバイスなど)、ルートデバイスが計算に使用されます。この
例では、非同期例外を処理する方法も示しています。
// Create sub devices for multi stack device
std::vector<sycl::device> subdev = {};sycl::queue subdev_queue0;
sycl::queue subdev_queue1;
try {using namespace sycl::info;
subdev = dev.create_sub_devices<
partition_property::partition_by_affinity_domain>(partition_affinity_domain::numa);
std::cout << "\tUse " << subdev.size() << " subdevices" << std::endl;} catch (sycl::exception &e) {std::cout << "\tSubdevices not found, use root device" << std::endl;}
int64_t nb_device = subdev.size();311

oneAPI GPU 最適化ガイド// Catch asynchronous exceptions
auto exception_handler = [](sycl::exception_list exceptions) {for (std::exception_ptr const &e : exceptions) {try {std::rethrow_exception(e);} catch (sycl::exception const &e) {std::cout << "Caught asynchronous SYCL exception during GEMM:\n"<< e.what() << std::endl;}}};

その後、コンテキストとキューが作成されます。サブデバイスが早期に検出された場合、サブデバイスごとに個別のキューが作成されます。
// Create context and execution queue
subdev.push_back(dev);
sycl::context cxt(subdev);
sycl::queue main_queue(cxt, dev, exception_handler);// If two subdevices are detected, create queue for each subdevice
if (nb_device > 1) {subdev_queue0 = sycl::queue(cxt, subdev[0], exception_handler);subdev_queue1 = sycl::queue(cxt, subdev[1], exception_handler);}

oneMKL 呼び出しが起動される前に、入力行列と出力行列を割り当てて、初期化してからホストからデバイスに転送する必要があります。2 つのサブデバイスが検出されると、最初のスタックで GEMM 計算の A 行列と B 行列の前半半分が実行され、後半は 2 番目のスタックで実行されます。したがって、それぞれに応じた行列を各スタックにコピーする必要があります。
// Allocate and initialize arrays for matrices
int64_t sizea;
if (transA == oneapi::mkl::transpose::nontrans)
sizea = ldA * k;else
sizea = ldA * m;
int64_t sizeb = ldB * n;
int64_t sizec = ldC * n;auto A_host = sycl::malloc_host<fp>(sizea, main_queue);auto B_host = sycl::malloc_host<fp>(sizeb, main_queue);auto C_host = sycl::malloc_host<fp>(sizec, main_queue);
init_matrix(A_host, transA, m, k, ldA);
init_matrix(B_host, transB, k, n, ldB);
init_matrix(C_host, oneapi::mkl::transpose::nontrans, m, n, ldC);// Copy A/B/C from host to device/subdevices// When two stacks are detected,// GEMM operation is split between stacks in n direction// All A matrix is copied to both stacks.
// B and C matrices are split between stacks, so only half of B and C are// copied to each stack.
fp *A_dev0, *A_dev1, *B_dev0, *B_dev1, *C_dev0, *C_dev1, *A_dev, *B_dev,*C_dev;
if (nb_device > 1) {A_dev0 = sycl::malloc_device<fp>(sizea, subdev_queue0);
A_dev1 = sycl::malloc_device<fp>(sizea, subdev_queue1);
B_dev0 = sycl::malloc_device<fp>(sizeb / 2, subdev_queue0);
B_dev1 = sycl::malloc_device<fp>(sizeb / 2, subdev_queue1);
C_dev0 = sycl::malloc_device<fp>(sizec / 2, subdev_queue0);312

oneAPI GPU 最適化ガイド
C_dev1 = sycl::malloc_device<fp>(sizec / 2, subdev_queue1);// Entire A matrix is copied to both stacks
subdev_queue0.memcpy(A_dev0, A_host, sizea * sizeof(fp));
subdev_queue1.memcpy(A_dev1, A_host, sizea * sizeof(fp));// Half of B and C are copied to each stack
subdev_queue0.memcpy(B_dev0, B_host, (sizeb / 2) * sizeof(fp));
subdev_queue1.memcpy(B_dev1, B_host + ldB * n / 2,
(sizeb / 2) * sizeof(fp));
subdev_queue0.memcpy(C_dev0, C_host, (sizec / 2) * sizeof(fp));
subdev_queue1.memcpy(C_dev1, C_host + ldC * n / 2,
(sizec / 2) * sizeof(fp));
subdev_queue0.wait();subdev_queue1.wait();} else {A_dev = sycl::malloc_device<fp>(sizea, main_queue);
B_dev = sycl::malloc_device<fp>(sizeb, main_queue);
C_dev = sycl::malloc_device<fp>(sizec, main_queue);
main_queue.memcpy(A_dev, A_host, sizea * sizeof(fp));
main_queue.memcpy(B_dev, B_host, sizeb * sizeof(fp));
main_queue.memcpy(C_dev, C_host, sizec * sizeof(fp));
main_queue.wait();}

最後に、oneMKL GEMM API が対応するデバイス/サブデバイスのキューを使用して呼び出され、GEMM 呼び出しが完了すると、更新された C 行列がホストに転送されます。
// Call oneMKL GEMM API// When two stacks are detected, GEMM call is launched on each stacksycl::event gemm_done0;
sycl::event gemm_done1;
std::vector<sycl::event> gemm_dependencies;try {if (nb_device > 1) {// Split B and C for each stack
int64_t n_subdev = n / 2;
gemm_done0 = oneapi::mkl::blas::gemm(
subdev_queue0, transA, transB, m, n_subdev, k, alpha, A_dev0, ldA,
B_dev0, ldB, beta, C_dev0, ldC, gemm_dependencies);
gemm_done1 = oneapi::mkl::blas::gemm(
subdev_queue1, transA, transB, m, n_subdev, k, alpha, A_dev1, ldA,
B_dev1, ldB, beta, C_dev1, ldC, gemm_dependencies);} else {gemm_done0 = oneapi::mkl::blas::gemm(main_queue, transA, transB, m, n, k,alpha, A_dev, ldA, B_dev, ldB, beta,
C_dev, ldC, gemm_dependencies);}} catch (sycl::exception const &e) {std::cout << "\t\tCaught synchronous SYCL exception during GEMM:\n"<< e.what() << std::endl;}// Wait for GEMM calls to finish
gemm_done0.wait();if (nb_device > 1)
gemm_done1.wait();// Copy C from device/subdevices to hostif (nb_device > 1) {subdev_queue0.memcpy(C_host, C_dev0, (sizec / 2) * sizeof(fp));
subdev_queue1.memcpy(C_host + ldC * n / 2, C_dev1,(sizec / 2) * sizeof(fp));
subdev_queue0.wait();subdev_queue1.wait();} else {main_queue.memcpy(C_host, C_dev, sizec * sizeof(fp));
main_queue.wait();}313

oneAPI GPU 最適化ガイド
この例を実行するには、MKLROOT 環境変数を設定した後に次のビルドコマンドを実行します。
$ icpx -fsycl -qmkl=sequential -DMKL_ILP64 -L${MKLROOT}/lib/intel64 -lsycl -lOpenCL lpthread -lm -ldl -o onemkl_dpcpp_gemm onemkl_dpcpp_gemm.cpp$ export LD_LIBRARY_PATH=${MKLROOT}/lib/intel64/:${LD_LIBRARY_PATH}$ ./onemkl_dpcpp_gemm

問題のサイズが大きい (m=n=k=8192) の場合、2 つのスタックで明示的にスケーリングすると、2 倍近いパフォーマ
ンスのスケーリングが期待できます。

5-16-5-2

OpenMP* でのインテル® oneAPI マス・カーネル・ライブラリー (oneMKL) によるパフォーマンスのスケーリング

こ の セ ク シ ョ ン で は 、 OpenMP* オ フ ロ ー ド の 例 と し て 、 マ ル チ ・ ス タ ッ ク ・ プ ラ ッ ト フ ォ ー ム 上 で イ ン テ ル ®
oneAPI マス・カーネル・ライブラリー (oneMKL) で明示的なスケーリングを行う方法について説明します。
次のコードは、AXPY の計算を 2 つのスタックに分割する方法を示しています。この例では、cblas_daxpy API
による明示的なスケーリングを示します。
#include "mkl.h"#include "mkl_omp_offload.h"#include <omp.h>#include <stdio.h>
int main() {double *x, *y, *y_ref, alpha;
MKL_INT n, incx, incy, i;// Initialize data for AXPY
alpha = 1.0;
n = 8192;
incx = 1;
incy = 1;// Allocate and initialize arrays for vectors
x = (double *)mkl_malloc(n * sizeof(double), 128);y = (double *)mkl_malloc(n * sizeof(double), 128);
if ((x == NULL) || (y == NULL)) {printf("Error in vector allocation\n");return 1;}
for (i = 0; i < n; i++) {x[i] = rand() / (double)RAND_MAX - .5;
y[i] = rand() / (double)RAND_MAX - .5;}
printf("First 10 elements of the output vector Y before AXPY:\n");for (i = 0; i < 10; i++) {printf("%lf ", y[i]);}printf("\n\n");// Detect number of available stacks
int nb_device = omp_get_num_devices();// Copy data to device and perform computation
if (omp_get_num_devices() > 1) {314

oneAPI GPU 最適化ガイド
printf("2 stacks are detected. AXPY operation is divided into 2 to take ""advantage of explicit scaling\n");printf("Copy x[0..%lld] and y[0..%lld] to stack 0\n", n / 2 - 1, n / 2 - 1);#pragma omp target data map(to\: x [0:n / 2]) map(tofrom\: y [0:n / 2]) device(0){printf("Copy x[%lld..%lld] and y[%lld..%lld] to stack 1\n", n / 2, n - 1,
n / 2, n - 1);#pragma omp target data map(to\: x [n / 2:n - n / 2]) map(tofrom\: y [n / 2:n - n / 2]) \device(1){double *x1 = &x[n / 2];
double *y1 = &y[n / 2];#pragma omp target variant dispatch use_device_ptr(x, y) device(0) nowait
cblas_daxpy(n / 2, alpha, x, incx, y, incy);#pragma omp target variant dispatch use_device_ptr(x1, y1) device(1)cblas_daxpy(n / 2, alpha, x1, incx, y1, incy);#pragma omp taskwait}}} else {printf("1 stack is detected. Entire AXPY operation is performed on that ""stack\n");printf("Copy x[0..%lld] and y[0..%lld] to stack 0\n", n - 1, n - 1);#pragma omp target data map(to : x [0:n]) map(tofrom : y [0:n]) device(0){#pragma omp target variant dispatch use_device_ptr(x, y) device(0)cblas_daxpy(n, alpha, x, incx, y, incy);#pragma omp taskwait}}// End of computation
printf("\nFirst 10 elements of the output vector Y after AXPY:\n");for (i = 0; i < 10; i++) {printf("%lf ", y[i]);}printf("\n");mkl_free(x);mkl_free(y);return 0;}

この例では、最初に AXPY のパラメーターとベクトルが割り当てられ、初期化されます。
// Initialize data for AXPY
alpha = 1.0;
n = 8192;
incx = 1;
incy = 1;// Allocate and initialize arrays for vectors
x = (double *)mkl_malloc(n * sizeof(double), 128);y = (double *)mkl_malloc(n * sizeof(double), 128);
if ((x == NULL) || (y == NULL)) {printf("Error in vector allocation\n");return 1;}
for (i = 0; i < n; i++) {x[i] = rand() / (double)RAND_MAX - .5;
y[i] = rand() / (double)RAND_MAX - .5;}
printf("First 10 elements of the output vector Y before AXPY:\n");315

oneAPI GPU 最適化ガイド
for (i = 0; i < 10; i++) {printf("%lf ", y[i]);}printf("\n\n");

次に、omp_get_num_devices API を使用して、利用可能なスタック数を検出します。
// Detect number of available stacks
int nb_device = omp_get_num_devices();

最後に、データが転送され、OpenMP* から oneMKL cblas_daxpy 呼び出しが GPU にオフロードされます。2 つのスタックが検出されると、x ベクトルと y ベクトルの半分ずつがそれぞれのスタックにコピーされ、明示的なスケーリ
ングのため AXPY 計算が 2 つに分割されます。
// Copy data to device and perform computation
if (omp_get_num_devices() > 1) {printf("2 stacks are detected. AXPY operation is divided into 2 to take ""advantage of explicit scaling\n");printf("Copy x[0..%lld] and y[0..%lld] to stack 0\n", n / 2 - 1, n / 2 - 1);#pragma omp target data map(to\: x [0:n / 2]) map(tofrom\: y [0:n / 2]) device(0){printf("Copy x[%lld..%lld] and y[%lld..%lld] to stack 1\n", n / 2, n - 1,
n / 2, n - 1);#pragma omp target data map(to\: x [n / 2:n - n / 2]) map(tofrom\: y [n / 2:n - n / 2]) \device(1){double *x1 = &x[n / 2];
double *y1 = &y[n / 2];#pragma omp target variant dispatch use_device_ptr(x, y) device(0) nowait
cblas_daxpy(n / 2, alpha, x, incx, y, incy);#pragma omp target variant dispatch use_device_ptr(x1, y1) device(1)cblas_daxpy(n / 2, alpha, x1, incx, y1, incy);#pragma omp taskwait}}} else {printf("1 stack is detected. Entire AXPY operation is performed on that ""stack\n");printf("Copy x[0..%lld] and y[0..%lld] to stack 0\n", n - 1, n - 1);#pragma omp target data map(to : x [0:n]) map(tofrom : y [0:n]) device(0){#pragma omp target variant dispatch use_device_ptr(x, y) device(0)cblas_daxpy(n, alpha, x, incx, y, incy);#pragma omp taskwait}}

この例を実行するには、MKLROOT 環境変数を設定した後に次のビルドコマンドを使用します。
$ icpx -fiopenmp -fopenmp-targets=spir64 -m64 -DMKL_ILP64 -qmkl-ilp64=parallel \-lstdc++ -o onemkl_openmp_axpy onemkl_openmp_axpy.cpp$ export LD_LIBRARY_PATH=${MKLROOT}/lib/intel64/:${LD_LIBRARY_PATH}$ ./onemkl_openmp_axpy

大きな問題サイズ (m=150000000) の場合、2 つのスタックで明示的にスケーリングすると、2 倍近いパフォーマン
スのスケーリングが期待できます。

316

oneAPI GPU 最適化ガイド

5-16-6

インテル® MPI ライブラリーを使用

GPU は HPC クラスターで一般的になり、インテル® MPI ライブラリーは、ヘテロジニアス・マシンでの大規模アプリ
ケーションのスケールアップおよびスケールアウト実行をサポートするようになりました。
GPU サポートは、インテル® MPI ライブラリー 2019 Update 8 で導入されました (「インテル®MPI ライブラリー・リ
リースノート」 (英語) を参照)。その後、いくつかの機能強化が行われています。特に明記しない限り、この節で示すすべての検証はインテル® MPI ライブラリー 2021.8 を使用しています。


MPI アプリケーションを GPU 上で実行
MPI + OpenMP* プログラムでのデバイス内およびデバイス間のデータ転送

5-16-6-1
1.

参考資料インテル®MPI ライブラリー・リリースノート

5-16-6-2

MPI アプリケーションを GPU 上で実行

この節では、インテル® MPI ライブラリーにおける以下の GPU 固有の機能について説明します。
•
•
•
•
•
•
•

トポロジーの検出
実行モデル
GPU のピニング
マルチスタック、マルチ GPU、およびマルチボードのサポート
インテル® MPI ベンチマークでの GPU ベンチマーク
GPU で MPI アプリケーションをプロファイル
推奨事項

5-16-6-2-1

トポロジーの検出

次の図では、2 つの CPU ソケットを備えたシステムの一般的な GPU 計算ノードを示しています。各 CPU ソケットには N 個のコアがあり、M 個のスタックを持つ単一の GPU カードに接続され、ノードあたり ２N のコアと 2M のスタッ
クがあります。

317

oneAPI GPU 最適化ガイド
一般的な GPU 計算ノード

上記のトポロジーにはバリエーションがあります。cpuinfo コマンドを使用して、詳細を表示します。
$ cpuinfo -gIntel(R) processor family information utility, Version 2021.11 Build
20231005 (id: 74c4a23)
Copyright (C) 2005-2021 Intel Corporation. All rights reserved.
===== Processor composition =====
Processor name : Intel(R) Xeon(R) Gold 8480
Packages(sockets) : 2
Cores : 112
Processors(CPUs) : 224
Cores per package : 56
Threads per core : 2

この例では、ソケットあたり 56 コアのデュアルソケットのノードがあり、ノードあたり合計 112 個の物理コアがあります。GPU トポロジーをさらに理解するには、インテル® MPI ライブラリーのデバッグ出力を使用できます。これを有効にするには、2 つの環境変数 (I_MPI_DEBUG = 3 以上、I_MPI_OFFLOAD = 1) を設定してアプリケーションを実行します。
[0] MPI startup(): ===== GPU topology on host1 =====[0] MPI startup(): NUMA Id GPU Id Stacks Ranks on this NUMA
[0] MPI startup(): 00(0,1) 0,1[0] MPI startup(): 1
1(2,3) 2,3

上記の省略された出力は、2 つの GPU と、GPU ごとに 2 つのスタックがあることを示しています。スタック 0 と 1 はGPU 0 に属し、スタック 2 と 3 は GPU 1 に属します。

5-16-6-2-2

実行モデル

開発者は、さまざまな方法で GPU 環境でインテル® MPI ライブラリーを利用できます。最も一般的ないくつかのシナリオを以下に示します。

318

oneAPI GPU 最適化ガイド
ナイーブ (自然な)このモデルでは、MPI 関数はホスト (汎用 CPU) のみで実行され、ベースとなる MPI ライブラリーはデバイス (ここでは GPU) を認識する必要はありません。下図に示すように、開発者はホストとデバイス間のデータ移動 (to: とtofrom: を使用) を処理する責任があります。このモデルでは、MPI 通信を頻繁に行うアプリケーションでは、ホストとデバイス間での過度なデータ移動が発生する可能性があります。ただし、このアプローチはメッセージサイズが小さな場合は有益です。
OpenMP* を使用したナイーブ実行モデル

上図では、MPI_Send が両方のオフロードプラグマの範囲外にあることに注意してください。ホストとデバイス間のデータ移動には、SYCL* と OpenMP* オフロード構造の両方が使用できます。
GPU を意識した MPI
このモデルでは、ベースとなる MPI ライブラリーが GPU バッファーを認識するため、開発者はホストとデバイス間で明示的なデータ移動タスクから解放されます。ただし、オフロード可能なメモリー領域のポインターを MPI に渡すには、特定のコンパイラー・ディレクティブを使用するか、対応するアクセラレーターのランタイム API から取得する必要があります。インテル® MPI ライブラリーは、必要に応じてデータを移動するだけではなく、ホストとデバイス間のデー
タ移動を最適化し、データ移動のコストを可能な限り隠匿します。MPI 呼び出しはホスト上で実行されます。GPU バッ
ファーをサポートするため、I_MPI_OFFLOAD 環境変数が導入されました。次の図のように、MPI_Send は omp
target parallel for プラグマのスコープ外にありますが、データ移動を指示する target 句がスコープ内にあります。また、ポインター値は use_device_ptr と is_device_ptr キーワードでマークされ、ポインター値はインテル® MPI ライブラリーで適切に処理されます。インテル® MPI ライブラリーの GPU 対応機能は、SYCL* とOpenMP* オフロードを使用するアプリケーションで使用できます。
OpenMP* オフロードを使用した GPU 対応実行モデル

319

oneAPI GPU 最適化ガイド
ビルドコマンド$ mpiicx -qopenmp -fopenmp-targets=spir64 test.c -o test

ここで、-qopenmp は、LLVM で OpenMP* コンパイルをサポートするミドルエンドを有効にし ます。そして、-fopenmp-targets=spir64 は、コンパイラーが GPU デバイスバイナリーに x86 + SPIR64* ファットバイナ
リーを生成することを指示します。
実行コマンド$ export I_MPI_OFFLOAD=1$ mpiexec.hydra -n 2 ./test

I_MPI_OFFLOAD は、MPI 関数のデバイスバッファー処理を制御します。値を 1 に設定すると、libze_loader.soがロードされている前提でデバイスバッファーの処理が有効にされます。ライブラリーがロードされていない場合(これは予期されていません)、GPU バッファーのサポートは有効になりません。インテル® MPI ライブラリーがlibze_loader.so をロードするように強制するには、I_MPI_OFFLOAD を 2 に設定します。
カーネルベースの集合
GPU 対応の MPI モデルとは異なり、このモデルはデバイスによるカーネルの MPI スケールアップ実行を直接サポートします。このアプローチは、ホストで MPI 呼び出しが実行されたホストとデバイス間のデータ転送を抑制するのに役立ちます。インテル® oneAPI コレクティブ・コミュニケーション・ライブラリー (インテル® oneCCL) などの MPI に類似した実装では、集合呼び出しに似た機能がすでにサポートされています。

5-16-6-2-3

GPU のピニング

ヘテロジニアス (CPU + デバイス) MPI プログラムでは、CPU コアを MPI ランクに割り当てるだけではなく、利用可
能な GPU リソースをランクに割り当てる必要もあります。インテル® MPI ライブラリーのコンテキストは、デフォル
トでインテル製ディスクリート GPU の基本ワークの単位はスタックであると想定します。ユーザーが必要とするピニ
ングスキームを実現するため、いくつかの環境変数が導入されています。インテル® MPI ライブラリーは、デフォルト
でバランスが取れたピニングスキームを適用します。これにより、ノードあたりのランク数がスタック数と等しい場合、
スタックごとに 1 つのランクが割り当てられます。オーバーサブスクリプションの場合、ラウンドロビン・スキームにより、最適なバランスの分散が行われます。インテル® MPI ライブラリーは、GPU ピニングを強制するため GPU のト
ポロジーを必要とします。これは、インテル® oneAPI レベルゼロ (英語) を使用して取得できます。次の環境変数は、トポロジーの検出にレベルゼロを使用することを指定します。
$ export I_MPI_OFFLOAD_TOPOLIB=level_zero

Intel® MPI ライブラリー 2021 Update6 以降では、I_MPI_OFFLOAD > 0 を設定すると、
I_MPI_OFFLOAD_TOPOLIB は自動的に level_zero に設定されます。さらに、GPU ピンニングを完全に無効にする必要がある場合、必要に応じてこのサポートを切り替えるため I_MPI_OFFLOAD_PIN 環境変数を使用できます。
デバッグレベル 3 (I_MPI_DEBUG=3) では、インテル® MPI ライブラリーは、最初のホストのみに対し実行時に使用される GPU ピンニングスキーム (GPU トポロジーに加えて) も出力します。すべてのホスト/ノードからのトポロジーとデバッグ情報を取得するには、デバッグレベル 120 以上を使用してください。
$ export I_MPI_DEBUG=3320

oneAPI GPU 最適化ガイド
以下は、4 スタックのマシンで、4 つのランクを実行するアプリケーションのデバッグ出力の例です。
[0] MPI startup(): ===== GPU topology on host1 =====[0] MPI startup(): NUMA Id GPU Id Stacks Ranks on this NUMA
[0] MPI startup(): 00(0,1) 0,1[0] MPI startup(): 1
1(2,3) 2,3[0] MPI startup(): ===== GPU pinning on host1 =====[0] MPI startup(): Rank Pin stack[0] MPI startup(): 0{0}[0] MPI startup(): 1{1}[0] MPI startup(): 2{2}[0] MPI startup(): 3{3}「GPU topology on host1」にある情報は、GPU のピニング情報として扱うべきではありません。これは、ランクが
NUMA ノードにどのように配置されるかを示します。ソケットあたり 1 つの NUMA ドメインしかないノードでは、
NUMA ノードは CPU ソケットとして扱うことができます。この前提で、上記のデバッグ情報は次のように解釈できま
す。
•
•
•

GPU 0 は CPU ソケット 0 に接続される。
GPU 0 はスタック 0 と 1 を持つ。
ランク 0 と 1 は CPU ソケット 0 に配置される。

同様に、
•
•
•

GPU 1 は CPU ソケット 1 に接続される。
GPU 1 はスタック 2 と 3 を持つ。
ランク 2 と 3 は CPU ソケット 1 に配置される。

「GPU Pinning on host1」の下の情報は、ランクから GPU スタックへのピニングを明確に示しています。ここでは、各ランクが固有のスタックに固定されています。同じことが次の図にも示されています。
デフォルトのピニングスキームの表現図明確にするため、別の例を考えてみます。上の図で示すノードに対し、実行を GPU 0 のみに限定するため、次の環境変数を設定します。

321

oneAPI GPU 最適化ガイド
ユーザー定義のピニングスキームの表現図$ export I_MPI_OFFLOAD_DEVICES=0

次のデバッグレベル 3 の出力にあるように、ランク 0、1 およびランク 2、３ は、それぞれ CPU ソケット 0 と 1 にあります。I_MPI_OFFLOAD_DEVICES が 0 に設定されているため、すべてのランクがラウンドロビン方式で GPU 0 のスタックに固定されています。上の図を参照してください。
[0] MPI startup(): ===== GPU topology on host1 =====[0] MPI startup(): NUMA Id GPU Id Stacks Ranks on this NUMA
[0] MPI startup(): 00(0,1) 0,1[0] MPI startup(): 1
1(2,3) 2,3[0] MPI startup(): ===== GPU pinning on host1 =====[0] MPI startup(): Rank Pin stack[0] MPI startup(): 0{0}[0] MPI startup(): 1{1}[0] MPI startup(): 2{0}[0] MPI startup(): 3{1}

CPU ０ へ の カ ス タ ム ピ ニ ン グ の ス キ ー ム を 定 義 す る I_MPI_PIN_PROCESOR_LIST 環 境 変 数 と 同 様 に 、
I_MPI_OFFLOAD_DEVICE_LIST 環境変数も GPU のピニングに使用できます。さらに詳細なピニング制御には、
I_MPI_OFFLOAD_CELL、I_MPI_OFFLOAD_DOMAIN_SIZE、I_MPI_OFFLOAD_DOMAIN といった環境変数を使用します。詳細は、「GPU ピニング」 (英語) のドキュメントを参照してください。

5-16-6-2-4

マルチスタック、マルチ GPU、およびマルチノードのサポート

最新のヘテロジニアス (CPU + GPU) クラスターには、さまざまな GPU 固有のスケーリングの可能性があります。
1.

スタック・スケーリング: 同一 GPU カード内のスタック間でのスケーリング (GPU 内、スタック間)。

2.

スケールアップ: 同じノードに接続された複数の GPU カード間でのスケーリング (ノード内、GPU 間)。

3.

スケールアウト: 異なるノード間に接続された GPU カード間のスケーリング (GPU 間、ノード間)。

これらのシナリオはすべてインテル® MPI ライブラリーによって透過的に処理されるため、アプリケーション開発者はソースコードを変更する必要はありません。次に、それぞれのシナリオのコマンドラインを示します。
ベースライン: すべてのランクをスタック 0 で実行$ I_MPI_OFFLOAD_DEVICE_LIST=0 I_MPI_DEBUG=3 I_MPI_OFFLOAD=1
mpiexec.hydra -n 2 ./mpi-binary[0] MPI startup(): ===== GPU topology on host1 =====

322

oneAPI GPU 最適化ガイド[0] MPI startup(): NUMA Id GPU Id Stacks Ranks on this NUMA
[0] MPI startup(): 0 0 (0,1) 0[0] MPI startup(): 1 1 (2,3) 1[0] MPI startup(): ===== GPU pinning on host1 =====[0] MPI startup(): Rank Pin stack[0] MPI startup(): 0 {0}[0] MPI startup(): 1 {0}

スタックのスケーリング: GPU 0 のスタックごとに 1 ランクを実行$ I_MPI_OFFLOAD_DEVICE_LIST=0,1 I_MPI_DEBUG=3
I_MPI_OFFLOAD=1 mpiexec.hydra -n 2 ./mpi-binary[0] MPI startup(): ===== GPU topology on host1 =====[0] MPI startup(): NUMA Id GPU Id Stacks Ranks on this NUMA
[0] MPI startup(): 0 0 (0,1) 0[0] MPI startup(): 1 1 (2,3) 1[0] MPI startup(): ===== GPU pinning on host1 =====[0] MPI startup(): Rank Pin stack[0] MPI startup(): 0 {0}[0] MPI startup(): 1 {1}スケールアップ: GPU 0 と GPU 1 のスタックごとに 1 ランクを実行$ I_MPI_DEBUG=3 I_MPI_OFFLOAD=1
mpiexec.hydra -n 4 ./mpi-binary[0] MPI startup(): ===== GPU topology on host1 =====[0] MPI startup(): NUMA Id GPU Id Stacks Ranks on this NUMA
[0] MPI startup(): 0 0 (0,1) 0,1[0] MPI startup(): 1 1 (2,3) 2,3[0] MPI startup(): ===== GPU pinning on host1 =====[0] MPI startup(): Rank Pin stack[0] MPI startup(): 0 {0}[0] MPI startup(): 1 {1}[0] MPI startup(): 2 {2}[0] MPI startup(): 3 {3}スケールアウト: ホスト 1 の GPU 0 と １、ホスト 2 の GPU 2 と 3 上のスタックごとに 1 ランクを実行$ I_MPI_DEBUG=3 I_MPI_OFFLOAD=1
mpiexec.hydra -n 8 -ppn 4 -hosts host1,host2 ./mpi-binary[0] MPI startup(): ===== GPU topology on host1 =====[0] MPI startup(): NUMA Id GPU Id Stacks Ranks on this NUMA
[0] MPI startup(): 0 0 (0,1) 0,1[0] MPI startup(): 1 1 (2,3) 2,3[0] MPI startup(): ===== GPU pinning on host1 =====[0] MPI startup(): Rank Pin stack[0] MPI startup(): 0 {0}[0] MPI startup(): 1 {1}[0] MPI startup(): 2 {2}[0] MPI startup(): 3 {3}ここには示しませんが、同様のピニングが host2 でも再現しました。

323

oneAPI GPU 最適化ガイド

5-16-6-2-5

FLAT モードでの考慮事項

デバイス階層のセクションで説明したように、現在の GPU ドライバーはデフォルトで FLAT モードを提供しています。
ZE_FLAT_DEVICE_HIERARCHY 環境変数を使用すると、FLAT モードと COMPOSITE モードを切り替えることができます。FLAT モードでは、インテル® MPI ライブラリーによって提示されるノードトポロジーは、sycl-ls や clinfoなどのツールと一致しません。これはバグではなく、インテル® MPI ライブラリー内の設計上の選択に過ぎません。
インテル® MPI ライブラリーは GPU スタックを計算の基本単位として選択し、選択したモードに関わりなく、常に実際のノードトポロジーを参照します。これにより、使用可能なスタック間でランクが均一に分散され、システム全体の使用率が最大化されます。
以下は、FLAT モードの 4 つのインテル® データセンター GPU MAX 1550 GPU を搭載したシステムの例です。syclls は 8 つの GPU デバイスを列挙し、インテル® MPI ライブラリーは 4 つの GPU デバイスを列挙します。
$ sycl-ls | grep GPU[ext_oneapi_level_zero:gpu:0] Intel(R) Level-Zero, Intel(R) Data Center GPU Max 1550
1.3 [1.3.27191][ext_oneapi_level_zero:gpu:1] Intel(R) Level-Zero, Intel(R) Data Center GPU Max 1550
1.3 [1.3.27191][ext_oneapi_level_zero:gpu:2] Intel(R) Level-Zero, Intel(R) Data Center GPU Max 1550
1.3 [1.3.27191][ext_oneapi_level_zero:gpu:3] Intel(R) Level-Zero, Intel(R) Data Center GPU Max 1550
1.3 [1.3.27191][ext_oneapi_level_zero:gpu:4] Intel(R) Level-Zero, Intel(R) Data Center GPU Max 1550
1.3 [1.3.27191][ext_oneapi_level_zero:gpu:5] Intel(R) Level-Zero, Intel(R) Data Center GPU Max 1550
1.3 [1.3.27191][ext_oneapi_level_zero:gpu:6] Intel(R) Level-Zero, Intel(R) Data Center GPU Max 1550
1.3 [1.3.27191][ext_oneapi_level_zero:gpu:7] Intel(R) Level-Zero, Intel(R) Data Center GPU Max 1550
1.3 [1.3.27191]$ I_MPI_DEBUG=3 I_MPI_OFFLOAD=1 mpirun -n 8 IMB-MPI1-GPU[0] MPI startup(): ===== GPU topology on host1 =====[0] MPI startup(): NUMA nodes : 2[0] MPI startup(): GPUs
: 4[0] MPI startup(): Tiles: 8[0] MPI startup(): NUMA Id
GPU IdTiles
this NUMA[0] MPI startup(): 00,1(0,1)(2,3)[0] MPI startup(): 1
2,3(4,5)(6,7)

Ranks on
0,1,2,3
4,5,6,7

FLAT モードでは、sycl-ls は 8 つの GPU、つまり各スタックを個別のデバイス (gpu:0 から gpu:7) として列挙しますが、インテル® MPI ライブラリーは 4 つの GPU または 8 つのスタック (タイル) を表示します。COMPOSITE モー
ドでは、インテル® MPI ライブラリーと sycl-ls または clinfo の出力に違いはありません。
また、トポロジー検出、ピンニング、パフォーマンスに関するインテル® MPI ライブラリーの動作は、FLAT モードとCOMPOSITE モードで変化しないことにも注意してください。

324

oneAPI GPU 最適化ガイド

5-16-6-2-6

環境変数に関する注意事項

これまで使用してきたワークフローでは、レベルゼロ・バックエンド、コンパイラー・ランタイム、インテル® MPI ライブ
ラリーのランタイムなど複数のコンポーネントが同時に動作しています。各コンポーネントは、ユーザーの要求に基づいてランタイム環境を制御するための環境変数 (ノブ) をいくつか提供しています。異なるコンポーネントから競合するノブ設定が行われる場合があります。このような状況に対処するため、一方が他方より優先されるように特定のルールが設定されています。よくあるシナリオは、レベルゼロ・バックエンド (ZE_AFFINITY_MASK など) とインテル®
MPI ライブラリー (I_MPI_OFFLOAD_CELL_LIST など) の競合するアフィニティー・ノブを使用する場合です。このようなシナリオでは、レベル・ゼロ・ノブがインテル® MPI ライブラリー・ノブよりも優先されます。また、競合する環境変数を解決するのが難しく、そのような場合にランタイムエラーが発生することもあります。したがって、使用されている環
境変数をよりよく理解するため、製品のドキュメントを参照することを推奨します。また、すべての環境変数が必須というわけではなく、関係するランタイムへの要求であるため、要求された設定が遵守されていることを確認するデバッグ
出力を生成して分析することをお勧めします。

5-16-6-2-7

インテル® MPI ベンチマークの GPU ベンチマーク

インテル® MPI ベンチマーク (IMB) (英語) は、さまざまなメッセージサイズでポイントツーポイントおよび集合通信操
作を行う一連の計測を行います。生成されたベンチマーク・データは、クラスターのパフォーマンス特性と MPI 実装の効率を評価するのに役立ちます。従来、IMB ベンチマークは CPU でのみ実行されていましたが、最近 GPU がサポートされるようになりました。IMB-MPI1-GPU と呼ばれるビルド済みバイナリーが、インテル® MPI ライブラリー・パッ
ケージに含まれます (Linux* マシンの $I_MPI_ROOT/bin フォルダー)。このバイナリーは、前述の GPU 対応 MPIモデルで実行できます。
以下は、デフォルトの 0 から 4MB のメッセージサイズで IMB-MPI1-GPU を実行し、2 つの GPU カードを搭載した単一ノードでメッセージサイズごとに 200 回の反復を行うコマンドラインの例です。ここでは、MPI_Allreduce 関
数のみが実行されていますが、ほかにも多くの MPI 関数がサポートされています。
$ I_MPI_DEBUG=3 I_MPI_OFFLOAD=1
mpiexec.hydra -n 4 -ppn 4
IMB-MPI1-GPU allreduce -mem_alloc_type device -npmin 4 -iter 200-iter_policy off-mem_alloc_type オプションは、MPI バッファーが存在する場所を制御します。インテル® MPI ライブラリー 2021
Update4 以降では、-mem_alloc_type のデフォルトは device です。
-npmin 4 は、IMB の MPI ランクの最小数を 4 に制限します (指定しない場合、-n で指定される MPI ランク数に段階的に到達します)。
-iter 200 は、メッセージサイズごとの反復回数を 200 に指定します。
-iter_policy off は 、 大 き な メ ッ セ ー ジ サ イ ズ の 反 復 の 自 動 リ ダ ク シ ョ ン を 無 効 に し ま す 。 つ ま り 、-iter_policy off は、すべてのメッセージサイズで、-iter 200 の要求に従うことを保証します。

325

oneAPI GPU 最適化ガイド
ライン飽和テスト
この項では、GPU バッファーを使用する際にインターコネクトの帯域幅を飽和させるインテル® MPI ライブラリーの機能を説明します。ここでは、デュアルソケットの インテル® Xeon® Platinum 8480+ プロセッサーと 2 つのインテル® データセンター GPU Max 1550 (Ponte Vecchio✝) でテストしています。ノードは、定格 200Gb/秒 もしくは 25GB/秒 の Mellanox Quantum* HDR インターコネクトで接続されています。次のコマンドに示すように、2 つの MPI ランク (ノードごとに 1 ランク) が起動され、IMB-MPI1-GPU バイナリーのピンポンテストが選択されています。
$ I_MPI_DEBUG=3 I_MPI_OFFLOAD=1 mpiexec.hydra -n 2 -ppn 1-hosts host1,host2 IMB-MPI1-GPU pingpong -iter 100 -iter_policy off-msglog 0:28

GPU バッファーを使用した IMB ベースのライン比率の飽和テスト。多くの要因がパフォーマンスに影響します。パフォーマンスは、ハードウェアとソフトウェアの構成によって異なります。測定されるパフォーマンスは、ここで示す測定値とは異なる場合があります。

こ こ で の ラ イ ン 飽 和 の テ ス ト に は 、 イ ン テ ル ® MPI ラ イ ブ ラ リ ー 2021 Update8 が 使 用 さ れ て い ま す 。
23,948.98Mb/秒のピーク帯域幅が計測されました。これは、93.6% のライン比率に相当します。

5-16-6-2-8

GPU で MPI アプリケーションをプロファイル

インテル® VTune™ プロファイラー、インテル® Advisor、そしてインテル® Inspector などのツールは、インテル® GPU
で動作するように拡張されています。CPU のプロファイルと同様に、これらのツールは、GPU コンテキストの MPI ア
プリケーションで引き続き利用できます。ここでは、それらの機能の一部を MPI アプリケーションのコンテキストで紹介します。
また、前の節で紹介した IMB-MPI1-GPU ベンチマークについて検討します。MPI バッファーが GPU メモリーに割り当てられているかどうかを確認するには、PCIe* トラフィックをチェックします。GPU は PCIe* バスを介して CPU に接続されるため (-mem_alloc_type CPU と比較して) トラフィックが増加します。インテル® VTune™ プロファイ
ラーの入出力解析では、いくつかの PCIe* メトリックをキャプチャーして表示できます。
326

oneAPI GPU 最適化ガイド
リモートマシン上のインテル® VTune™ プロファイラーを操作するモードがサポートされています。ここで示すプロ
ファイル・データは、リモートマシンのコマンドライン・インターフェイスから収集されたものです。リモートマシンの結果フォルダーは、ローカル GUI で表示するようローカルマシンに移動されます。次のコマンドラインは、GPU ノードを持つリモートマシンで実行されます。
CPU のバッファー割り当て$ I_MPI_OFFLOAD=1 mpiexec.hydra-n 4 -ppn 4 -f hostfile -gtool "vtune -collect io -r ./vtune_data1:0"
IMB-MPI1-GPU allreduce -npmin 4 -iter 200 -iter_policy off-mem_alloc_type CPU

GPU のバッファー割り当て$ I_MPI_OFFLOAD=1 mpiexec.hydra-n 4 -ppn 4 -f hostfile -gtool "vtune -collect io -r./vtune_data2:0" IMB-MPI1-GPU allreduce -npmin 4 -iter 200-iter_policy off

ここでは、-gtool オプションを使用して、ランク 0 のみでインテル® VTune™ プロファイラーを起動しています(-gtool の最後の引数 :0 でランクを指定)。
入出力解析の [Summary (サマリー)] タブの [Bandwidth Utilization Histogram (帯域幅利用率ヒストグラム)](下図) に示すように、MPI バッファーがデバイスに割り当てられていると、PCIe* トラフィックが大幅に増加していることが分かります。-mem_alloc_type device で取得された最大帯域幅は 24,020MB/秒であるのに対し、CPU
バッファーでは 27MB/秒でした。
CPU バッファー割り当てによる PCIe* 帯域幅の低い利用率

GPU バッファー割り当てによる PCIe* 帯域幅の高い利用率

327

oneAPI GPU 最適化ガイド
ホストとデバイス間で転送されるデータ量を確認するには、インテル® VTune™ プロファイラーの GPU オフロード解
析を使用します。図 7 に示すように、[Graphics (グラフィックス)] タブにホストからデバイス、およびデバイスからホストへの転送時間と転送量が表示されます。GPU オフロード解析を行うには、次のコマンドラインを使用します。
$ I_MPI_DEBUG=3 I_MPI_OFFLOAD=1
mpiexec.hydra -n 4 -ppn 4 -f
hostfile -gtool "vtune -collect gpu-offload -knob
collect-host-gpu-bandwidth=true -r ./vtune_data3:0" IMB-MPI1-GPU
allreduce -npmin 4 -iter 200 -iter_policy off

GPU オフロード解析

この項では、ヘテロジニアス変数で動作する MPI アプリケーションのコンテキストで、解析ツールが使用できることを実証します。インテル® VTune™ プロファイラーの GPU 固有の各種機能の詳細は、『インテル® VTune™ プロファイ
ラー・ユーザーガイド』を参照してください。前述したように、-gtool オプションを使用すると、インテル® Advisor やインテル® Inspector などほかの解析ツールも MPI アプリケーションで利用できます。詳細については、『インテル®
Advisor ユーザーガイド』、『インテル® Inspector ユーザーガイド (Linux* 版)』 (英語) を参照してください。

5-16-6-2-9

推奨事項

1.

MPI 通信では、可能な限り同じ GPU バッファーを再利用することを推奨します。インテル® MPI ライブラリーのバッファー作成はコストの高い操作であるため、必要のないバッファーの割り当てと解除は避けるべきです。

2.

アプリケーションが、非ブロッキングのポイントツーポイント操作に複数の GPU バッファーを使用する場合、インテル® MPI ライブラリーの並列バッファー数を増やすことでパフォーマンスを向上できます。これには、
環境変数 I_MPI_OFFLOAD_BUFFER_NUM を使用します。デフォルト値は 4 であり、インテル® MPI ライブラ
リーは 4 ユーザーのデバイスバッファーを並列に処理します。この値を増やすとメモリー消費量も多くなります。

3.

アプリケーションが大きなデバイスバッファーを使用する場合、インテル® MPI ライブラリーが使用するスク
ラッチバッファーのサイズを増やすことでパフォーマンスが向上することがあります。これには、環境変数
I_MPI_OFFLOAD_BUFFER_SIZE を使用します。デフォルト値は 8MB です。この値を増やすとメモリー消費量も多くなります。

4.

GPU 対応モデルでは、インテル® MPI ライブラリーはパフォーマンス向上のためパイプラインを使用します。
メッセージサイズが大きな場合に、環境変数 I_MPI_OFFLOAD_PIPELINE_THRESHOLD を使用して、パイプ
ラインのアルゴリズムを有効にするタイミングを制御できます。この変数のデフォルト値は 524288 バイトです。ユーザーは、利用環境に最適な値を設定できるようこの制御を最適化できます。

5.

環境変数 I_MPI_OFFLOAD_CACHE_TOTAL_MAX_SIZE を使用すると、インテル® MPI ライブラリーで使用するすべての内部キャッシュのサイズを増やすことができます。デフォルト値は 67108864 バイトです。アプリ
ケーションが多くの GPU バッファーを使用する場合、すべてをキャッシュできないため、この変数の値を増やすとパフォーマンスが向上する可能性があります。

5-16-6-2-10

参考資料

1.

インテル® MPI ライブラリー・リリースノート (英語)2.

インテル® oneAPI レベルゼロ (英語)328oneAPI GPU 最適化ガイド
3.

インテル® oneCCL (英語)4.

GPU のピニング (英語)5.

インテル® MPI ベンチマーク (英語)6.

インテル® VTune™ プロファイラー・ユーザーガイド

7.

インテル® Advisor ユーザーズガイド

8.

インテル® Inspector ユーザーズガイド Linux* (英語)

5-16-6-3

MPI + OpenMP* プログラムでのデバイス内およびデバイス間のデータ転送

一部のアプリケーションは、単一のプロセスが利用可能なすべての GPU リソースを使用する純粋な OpenMP* プロ
グラムです。ただし、他の多くのアプリケーションでは、複数の MPI ランクが使用され、これらの MPI ランクのそれぞれが OpenMP* を使用して GPU リソースの一部 (完全な GPU または GPU サブデバイス) にアクセスする MPI とOpenMP* ハイブリッド構造が使用されます。
通常、このようなアプリケーションでは、あるポイントで、異なるランクで使用される GPU リソース間でデータを交換する必要があります。これを行うには、通常、最初にデータを GPU から MPI 転送を行う CPU に転送し、ターゲット
CPU で受信されたら、ターゲット GPU に再度転送する必要があります。
原則として、同じ GPU のサブデバイス間でデータを転送する場合、データを CPU に転送する必要はありません。また、
GPU が直接データを交換できるインテル® Xe Link を使用するシステムの場合、これも必要ありません。
インテル® MPI ライブラリーに加えて、インテル® データセンター GPU マックス・シリーズ対応の MPICH ライブラ
リーは、GPU と GPU サブデバイス間の直接転送をサポートし、通信効率を向上できます。
このライブラリーの MPI プリミティブは、ポインターが CPU からも GPU 内のデータを指しているかどうかを判断できるため、CPU を介してデータを転送する必要なく、GPU デバイスまたはサブデバイス間でデータを直接転送する別の通信パスをアクティブ化できます。
これを前提として、ライブラリーをインストールした後、それを活用するには、OpenMP* API とディレクティブを使用してデバイスポインターを取得し、MPI 呼び出しで直接 GPU 通信パスをアクティブ化する必要があります。これには2 つのシナリオが考えられます。
1. omp_target_alloc によってデータがデバイスに直接割り当てられる場合。
omp_target_alloc ルーチン (または同様のルーチン) を使用してデバイスにデータが割り当てられると、返されるポインターは MPI 呼び出しで直接使用できるデバイス ポインターになります。例:
1.
2.

2.

double *dst_buff = omp_target_alloc(device_id, 1000 * sizeof(double));MPI_Recv(dst_buff,...);

データがデバイスにマップる場合。
データが map 句または OpenMP* の暗黙的なマッピングルールによりデバイスに割り当てられた場合は、
target data ディレクティブの use_device_ptr 句または use_device_addr 句を使用して、MPI 呼び出しで使用できるデバイス ポインターを取得する必要があります。例:
1.
2.

#pragma omp target data map(dst_buff){

329

oneAPI GPU 最適化ガイド
3.
4.
5.
6.
7.

#pragma omp target data use_device_addr(dst_buff){
MPI_Recv(dst_buff,...)}}

次に、複雑な例を見てみましょう。この例では、フラグを使用して、MPI 呼び出しでデバイスポインター (インテル® デー
タセンター GPU マックス・シリーズ対応の MPICH ライブラリーが使用されている場合) またはホストポインターを使用するかどうかをユーザーが選択できます。また、このコードを使用して、デバイス間転送を行うパフォーマンスの違いを紹介します。このコードは、いくつかのバッファーを複数の MPI ランク間で循環させながら、その値を増やしていきます。
mpi_aware 変数を使用して、デバイス間転送を使用するか、通常の MPI 転送を使用するかを制御します。
1.
2.
3.
4.
5.

int mpi_aware = 0;
if ( argc > 1 ) {mpi_aware = 1;
printf("MPI device aware path enabled\n");} // argc check

アプリケーションは buf1 と buf2 の 2 つのバッファーを使用するため、まず target data 構造を使用して、デバ
イス上で通常どおりにマッピングします。次に、条件付き target data 構造を使用して、mpi_aware が true の場合のみ、buf1 と buf2 のアドレスをデバイスアドレスに変換します。したがって、mpi_aware が true の場合、curr ポインターと next ポインターはデバイスアドレスを保持します。それ以外の場合は、ホストアドレスを保持します。これは、printf 文で確認できます。
1.
2.
3.
4.
5.
6.
7.
8.

#pragma omp target data map(buf1,buf2){#pragma omp target data use_device_addr(buf1,buf2) if(mpi_aware){
curr = buf1;
next = buf2;}
printf("curr=%p next=%p\n",curr,next);

mpi_aware が false の場合、printf は次のようなホストアドレスの値を出力します。
curr=0x7ffdffc11850 next=0x7ffdff470650

またｈ、mpi_aware が true の場合、printf は次のようなデバイスアドレスの値を出力します。
curr=0xff00000000200000 next=0xff00000000a00000

最後に、MPI 通信呼び出しの前後に 2 つの条件付き target update 構造を使用して、mpi_aware が false の場合にのみ GPU 変数を更新します。これは、デバイス間転送が使用される場合は必要がないためです。
1. if ( nranks > 1 ) {2.
#pragma omp target update from(curr[0:N]) if(!mpi_aware)3.
MPI_Request srq;
4.
MPI_Isend(curr,N,MPI_DOUBLE,next_rank,0,MPI_COMM_WORLD,&srq);
5. // we need to make sure that the MPI_Isend of the previous
6. // iteration finished before doing the MPI_Recv of this
7. // iteration8.
if ( step > 0 ) MPI_Wait(&psrq,MPI_STATUS_IGNORE);
9.
psrq = srq;
10.
MPI_Recv(next,N,MPI_DOUBLE,prev_rank,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
11.
#pragma omp target update to(next[0:N]) if(!mpi_aware)
12. } // nranks

330

oneAPI GPU 最適化ガイド
このプログラムを使用して、MPI ランクごとに 1 つずつ、2 つのサブデバイスに分割された単一の GPU を持つシステ
ムのパフォーマンスの違いを評価します。unitrace ツールで取得できる、ホストからデバイスへのメモリー操作数(M2D 操作) とその逆 (D2M 操作) を削減した結果により、大幅な時間差が確認できます。
2 MPI ランクバージョン

時間 (秒)

M2D 操作 (ランクごと)D2M 操作 (ランクごと)

mpi_aware = 0

3.07

1002

1002

mpi_aware = 1

0.13

2

2

これで、同じプログラムを使用して、インテル® Xe Link で接続された 2 つの GPU デバイスを備えたシステムのパフォーマンスの違いを評価できるようになりました。この場合、各 GPU は 2 つのサブデバイスとしてプログラムされるため、4 つの MPI ランクがあります。前のケースと同様に、ホストとデバイス間のメモリー操作の時間と回数が削減されていることがわかります。
4 MPI ランクバージョン

時間 (秒)

M2D 操作 (ランクごと)D2M 操作 (ランクごと)

mpi_aware = 0

3.45

1002

1002

mpi_aware = 1

0.44

2

2

これまで見てきたように、GPU をインテル® Xe Link に接続し、インテル® データセンター GPU マックス・シリーズ対応の MPICH ライブラリーを使用すると、GPU 間の通信効率が大幅に向上します。
注: デバイス間転送をサポートしていない MPI ライブラリーで mpi_aware パスを使用すると、プログラムが異常終了する可能性があります。

5-16-7
5-16-7-1

高度なトピック
コンピューティング・コマンド・ストリーマー (CCS)インテル® データセンター GPU マックスの各スタックには 4 つの Compute Command Streamer (CCS) が含まれており、これを使用して実行ユニット (EU) のプールにアクセスできます。
ハードウェアにより、CCS 間での EU の特定の配分を選択できます。スタック内の EU は、スタック内の 1 つの CCS、2
つの CCS、または 4 つの CCS に割り当てることができます。


1 CCS モード (デフォルト): このモードでは、各スタックの 1 つの CCS が公開されます。CCS はスタック内のす
べての EU にアクセスできます。その他の CCS は無効です。
2 CCS モード: このモードでは、各スタック内の 2 つの CCS が公開され、それぞれにスタック内の EU の半分が割り当てられます。いずれかの CCS の EU がアイドル状態の場合、その EU は他の CCS では使用できません。
331

oneAPI GPU 最適化ガイド
4 CCS モード: このモードでは、スタックの 4 つの CCS すべてが公開され、それぞれにスタック内の EU の 4 分の 1 が割り当てられます。2 CCS モードと同様に、アイドル状態の CCS の EU は他の CCS では使用できません。

デフォルトは 1 CCS モードです。
アプリケーションによっては、スタックごとに 1 つの CCS を使用してスタック内のすべての EU にアクセスするとメリットが得られる場合があります。一方、他のアプリケーションでは、スタックごとに 2 つまたは 4 つの CCS を使用して、各 CCS に EU のサブセットを割り当てるとメリットが得られる場合があります。
スタックごとに 2 つまたは 4 つの CCS を使用すると、スタック上で複数の小さなカーネルを同時に実行し、各カーネルによる計算にスタックのすべての計算リソース (EU) が必要ない場合に便利です。この場合、スタック内の異なる
CCS に異なるカーネルを送信して、カーネルを並列に実行できるようにすると有利です。
環境変数 ZEX_NUMBER_OF_CCS を使用すると、GPU カードの各スタックに公開される CCS の数を指定できます。
ZEX_NUMBER_OF_CCS の 形 式 は 、 デ バ イ ス ・ モ ー ド ・ ペ ア を コ ン マ で 区 切 っ た リ ス ト で す 。 つ ま り 、
ZEX_NUMBER_OF_CCS=<ルート・デバイス・インデックス>:<CCS モード>,<ルート・デバイス・インデックス>:<CCS
モード>… です。たとえば、スタックが 2 つある GPU カードでは、次のように指定してスタック 0 を 4-CCS モードに設定し、スタック 1 を 1-CCS モードに設定します。
ZEX_NUMBER_OF_CCS=0:4,1:1

以下に、SYCL*、OpenMP*、MPI アプリケーションで CCS を公開する例を示します。
SYCL* で複数の CCS を使用する
SYCL* では、CCS (サブサブデバイス) に関連付けられたコンテキストを作成し、プログラムで CCS レベルでのきめ細かい制御を行うことができます。次の例では、GPU カード (デバイス) 上のすべてのスタック (サブデバイス) と CCS (サブサブデバイス) を検索します。
1. #include <sycl/sycl.hpp>
2.
3. int main() {4.
// Find all GPU devices
5.
auto devices = sycl::platform(sycl::gpu_selector_v).get_devices();6.
for (int n = 0; n < devices.size(); n++) {7.
std::cout << "\nGPU" << n << ": "8.
<< devices[n].get_info<sycl::info::device::name>() << " ("9.
<< devices[n].get_info<sycl::info::device::max_compute_units>()
10.
<< ")\n";11.
std::vector<sycl::device> subdevices;12.
std::vector<sycl::device> subsubdevices;13.
auto part_prop =14.
devices[n].get_info<sycl::info::device::partition_properties>();15.
if (part_prop.empty()) {16.
std::cout << "No partition_properties\n";17.
} else {18.
for (int i = 0; i < part_prop.size(); i++) {19.
// Check if device can be partitioned into Tiles20.
if (part_prop[i] ==21.
sycl::info::partition_property::partition_by_affinity_domain) {22.
auto sub_devices =23.
devices[n]24.
.create_sub_devices<sycl::info::partition_property::
25.
partition_by_affinity_domain>(26.
sycl::info::partition_affinity_domain::numa);27.
for (int j = 0; j < sub_devices.size(); j++) {28.
subdevices.push_back(sub_devices[j]);332oneAPI GPU 最適化ガイド
29.
std::cout << "\ntile" << j << ": "30.
<< subdevices[j].get_info<sycl::info::device::name>()31.
<< " ("
32.
<< subdevices[j]33.
.get_info<sycl::info::device::max_compute_units>()
34.
<< ")\n";35.
auto part_prop1 =36.
subdevices[j]37.
.get_info<sycl::info::device::partition_properties>();38.
if (part_prop1.empty()) {39.
std::cout << "No partition_properties\n";40.
} else {41.
for (int i = 0; i < part_prop1.size(); i++) {42.
// Check if Tile can be partitioned into Slices (CCS)43.
if (part_prop1[i] == sycl::info::partition_property::44.
ext_intel_partition_by_cslice) {45.
auto sub_devices =46.
subdevices[j]47.
.create_sub_devices<48.
sycl::info::partition_property::49.
ext_intel_partition_by_cslice>();50.
for (int k = 0; k < sub_devices.size(); k++) {51.
subsubdevices.push_back(sub_devices[k]);52.
std::cout53.
<< "slice" << k << ": "54.
<< subsubdevices[k].get_info<sycl::info::device::name>()55.
<< " ("
56.
<< subsubdevices[k]57.
.get_info<
58.
sycl::info::device::max_compute_units>()
59.
<< ")\n";60.
}
61.
break;
62.
} else {63.
std::cout << "No ext_intel_partition_by_cslice\n";
64.
}
65.
}
66.
}
67.
}
68.
break;
69.
// Check if device can be partitioned into Slices (CCS)70.
} else if (part_prop[i] == sycl::info::partition_property::71.
ext_intel_partition_by_cslice) {72.
auto sub_devices =73.
devices[n]74.
.create_sub_devices<sycl::info::partition_property::75.
ext_intel_partition_by_cslice>();76.
for (int k = 0; k < sub_devices.size(); k++) {77.
subsubdevices.push_back(sub_devices[k]);78.
std::cout << "slice" << k << ": "79.
<< subsubdevices[k].get_info<sycl::info::device::name>()80.
<< " ("
81.
<< subsubdevices[k]82.
.get_info<sycl::info::device::max_compute_units>()
83.
<< ")\n";84.
}85.
break;
86.
} else {87.
std::cout << "No ext_intel_partition_by_cslice or "88.
"partition_by_affinity_domain\n";
89.
}
90.
}
91.
}
92.
}
93.
return 0;
94. }以下の SYCL* コードは、複数のカーネルを複数の CCS に送信して同時に実行する方法を示しています。
333

oneAPI GPU 最適化ガイド
サンプルコードはすべての CCS を検出し、GPU デバイスで見つかった各 CCS に対して sycl::queue を作成し、for ループを使用してすべての CCS にカーネルを送信します。
1. #include <sycl/sycl.hpp>
2.
3. static constexpr size_t N = 5280; // global size
4. static constexpr size_t B = 32;// WG size
5.
6. void kernel_compute_mm(sycl::queue &q, float *a, float *b, float *c, size_t n,7.
size_t wg) {8.
q.parallel_for(9.
sycl::nd_range<2>(sycl::range<2>{n, n}, sycl::range<2>{wg, wg}),
10.
[=](sycl::nd_item<2> item) {11.
const int i = item.get_global_id(0);12.
const int j = item.get_global_id(1);13.
float temp = 0.0f;
14.
for (int k = 0; k < N; k++) {15.
temp += a[i * N + k] * b[k * N + j];
16.
}
17.
c[i * N + j] = temp;
18.
});
19. }
20.
21. int main() {22.
auto start =23.
std::chrono::high_resolution_clock::now().time_since_epoch().count();
24.
25.
// find all CCS / Tiles in GPU
26.
auto device = sycl::device(sycl::gpu_selector_v);
27.
std::cout << "\nGPU: " << device.get_info<sycl::info::device::name>() << " ("28.
<< device.get_info<sycl::info::device::max_compute_units>()
29.
<< ")\n";30.
std::vector<sycl::device> subdevices;31.
std::vector<sycl::device> subsubdevices;32.
auto part_prop = device.get_info<sycl::info::device::partition_properties>();33.
if (part_prop.empty()) {34.
std::cout << "No partition_properties\n";35.
} else {36.
for (int i = 0; i < part_prop.size(); i++) {37.
// Check if device can be partitioned into Tiles38.
if (part_prop[i] ==39.
sycl::info::partition_property::partition_by_affinity_domain) {40.
auto sub_devices = device.create_sub_devices<41.
sycl::info::partition_property::partition_by_affinity_domain>(42.
sycl::info::partition_affinity_domain::numa);43.
for (int j = 0; j < sub_devices.size(); j++) {44.
subdevices.push_back(sub_devices[j]);45.
std::cout46.
<< "\nTile" << j << ": "47.
<< subdevices[j].get_info<sycl::info::device::name>() << " ("48.
<< subdevices[j].get_info<sycl::info::device::max_compute_units>()
49.
<< ")\n";50.
auto part_prop1 =51.
subdevices[j]52.
.get_info<sycl::info::device::partition_properties>();53.
if (part_prop1.empty()) {54.
std::cout << "No partition_properties\n";55.
} else {56.
for (int i = 0; i < part_prop1.size(); i++) {57.
// Check if Tile can be partitioned into Slices (CCS)58.
if (part_prop1[i] == sycl::info::partition_property::59.
ext_intel_partition_by_cslice) {60.
auto sub_devices = subdevices[j]61.
.create_sub_devices<62.
sycl::info::partition_property::63.
ext_intel_partition_by_cslice>();64.
for (int k = 0; k < sub_devices.size(); k++) {65.
subsubdevices.push_back(sub_devices[k]);334oneAPI GPU 最適化ガイド
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.

std::cout<< "Slice" << k << ": "<< subsubdevices[k].get_info<sycl::info::device::name>()<< " ("<< subsubdevices[k].get_info<sycl::info::device::max_compute_units>()<< ")\n";}break;} else {std::cout << "No ext_intel_partition_by_cslice\n";}}}}break;// Check if device can be partitioned into Slices (CCS)} else if (part_prop[i] == sycl::info::partition_property::
ext_intel_partition_by_cslice) {auto sub_devices = device.create_sub_devices<
sycl::info::partition_property::ext_intel_partition_by_cslice>();for (int k = 0; k < sub_devices.size(); k++) {subsubdevices.push_back(sub_devices[k]);std::cout << "Slice" << k << ": "<< subsubdevices[k].get_info<sycl::info::device::name>()<< " ("<< subsubdevices[k].get_info<sycl::info::device::max_compute_units>()<< ")\n";}break;} else {std::cout << "No ext_intel_partition_by_cslice or ""partition_by_affinity_domain\n";}}}// Set devices to submit compute kernel
std::vector<sycl::device> devices(1, device);
if (subsubdevices.size())
devices = subsubdevices;
else if (subdevices.size())
devices = subdevices;auto num_devices = devices.size();// Define matrices
float *matrix_a[num_devices];
float *matrix_b[num_devices];
float *matrix_c[num_devices];
float v1 = 2.f;float v2 = 3.f;for (int n = 0; n < num_devices; n++) {matrix_a[n] = static_cast<float *>(malloc(N * N * sizeof(float)));matrix_b[n] = static_cast<float *>(malloc(N * N * sizeof(float)));matrix_c[n] = static_cast<float *>(malloc(N * N * sizeof(float)));// Initialize matrices with valuesfor (int i = 0; i < N; i++)for (int j = 0; j < N; j++) {matrix_a[n][i * N + j] = v1++;
matrix_b[n][i * N + j] = v2++;
matrix_c[n][i * N + j] = 0.f;}}
float *da[num_devices];
float *db[num_devices];335

oneAPI GPU 最適化ガイド
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.

336

float *dc[num_devices];
std::vector<sycl::queue> q(num_devices);// create queues for each device
std::cout << "\nSubmitting Compute Kernel to Devices:\n";for (int i = 0; i < num_devices; i++) {q[i] = sycl::queue(devices[i]);std::cout<< "Device" << i << ": "<< q[i].get_device().get_info<sycl::info::device::name>() << " ("<< q[i].get_device().get_info<sycl::info::device::max_compute_units>()<< ")\n";}// device mem alloc for matrix a,b,c for each devicefor (int i = 0; i < num_devices; i++) {da[i] = sycl::malloc_device<float>(N * N, q[i]);db[i] = sycl::malloc_device<float>(N * N, q[i]);dc[i] = sycl::malloc_device<float>(N * N, q[i]);}// warm up: kernel submit with zero sizefor (int i = 0; i < num_devices; i++)kernel_compute_mm(q[i], da[i], db[i], dc[i], 0, 0);// kernel syncfor (int i = 0; i < num_devices; i++)q[i].wait();// memcpy for matrix and b to device alloc
for (int i = 0; i < num_devices; i++) {q[i].memcpy(&da[i][0], &matrix_a[i][0], N * N * sizeof(float));q[i].memcpy(&db[i][0], &matrix_b[i][0], N * N * sizeof(float));}// wait for copy to completefor (int i = 0; i < num_devices; i++)q[i].wait();// submit matrix multiply kernels to all devicesfor (int i = 0; i < num_devices; i++)kernel_compute_mm(q[i], da[i], db[i], dc[i], N, B);// wait for compute completefor (int i = 0; i < num_devices; i++)q[i].wait();// copy back result to hostfor (int i = 0; i < num_devices; i++)q[i].memcpy(&matrix_c[i][0], &dc[i][0], N * N * sizeof(float));// wait for copy to completefor (int i = 0; i < num_devices; i++)q[i].wait();// print first element of result matrix
std::cout << "\nMatrix Multiplication Complete\n";for (int i = 0; i < num_devices; i++)std::cout << "device" << i << ": matrix_c[0][0]=" << matrix_c[i][0] << "\n";for (int i = 0; i < num_devices; i++) {free(matrix_a[i]);free(matrix_b[i]);free(matrix_c[i]);sycl::free(da[i], q[i]);sycl::free(db[i], q[i]);sycl::free(dc[i], q[i]);}oneAPI GPU 最適化ガイド
204.
205. auto duration =206.
std::chrono::high_resolution_clock::now().time_since_epoch().count() 207.
start;
208. std::cout << "Compute Duration: " << duration / 1e+9 << " seconds\n";209. return 0;210. }

例をビルドするには、次のコマンドを実行します。
$ icpx -fsycl -o ccs ccs.cpp$ icpx -fsycl -o ccs_matrixmul ccs_matrixmul.cpp

ccs で 見 つ か っ た CCS の 数 と ccs_matrixmul で 並 列 実 行 さ れ る カ ー ネ ル の 数 は 、 環 境 変 数
ZEX_NUMBER_OF_CCS の設定によって異なります。
OpenMP* で複数の CCS を使用する
OpenMP* では、各スタック内の CCS をデバイスとして公開して、CCS レベルできめ細かなパーティショニングと制御を行うことができます。
CCS をデバイスとして公開するには、プログラムを実行する前に次の 2 つの環境変数のいずれかを設定する必要があります。
$ export ONEAPI_DEVICE_SELECTOR="*:*.*.*"

または$ LIBOMPTARGET_DEVICES=SUBSUBDEVICE

次の OpenMP* プログラムは、FLAT モードでの CCS の使用法を示しています。
まず、プログラムは omp_get_num_devices() を呼び出して、プラットフォームで使用可能なデバイスの数を決定します。次に、プログラムはカーネルを各デバイスにオフロードし、各カーネルは配列 A の異なるチャンクを初期化します。
omp_get_num_devices() は、使用可能なデバイスの合計数を返します。
target ディレクティブの device 句は、カーネルをオフロードするデバイスを指定するのに使用されます。
実行時に、環境変数 ONEAPI_DEVICE_SELECTOR=”:.*.*” (またはLIBOMPTARGET_DEVICES=SUBSUBDEVICE) が ZEX_NUMBER_OF_CCS とともに設定され、CCS がデバイスとして公開されます。
1. #include <stdlib.h>
2. #include <stdio.h>
3. #include <omp.h>
4.
5. #define SIZE 320
6.
7. int num_devices = omp_get_num_devices();
8. int chunksize = SIZE/num_devices;
9.
10. int main(void)
11. {
12.
int *A;

337

oneAPI GPU 最適化ガイド
13.
A = new int[sizeof(int) * SIZE];
14.
15.
printf ("num_devices = %d\n", num_devices);16.
17.
for (int i = 0; i < SIZE; i++)18.
A[i] = -9;19.
20.
#pragma omp parallel for21.
for (int id = 0; id < num_devices; id++) {22.
#pragma omp target teams distribute parallel for device(id) \23.
map(tofrom: A[id * chunksize : chunksize])24.
for (int i = id * chunksize; i < (id + 1) * chunksize; i++) {25.
A[i] = i;
26.
}
27.
}
28.
29.
for (int i = 0; i < SIZE; i++)30.
if (A[i] != i)31.
printf ("Error in: %d\n", A[i]);
32.
else
33.
printf ("%d\n", A[i]);34. }コンパイルコマンド:$ icpx -fiopenmp -fopenmp-targets=spir64 flat_openmp_02.cpp

実行コマンド:$ OMP_TARGET_OFFLOAD=MANDATORY ONEAPI_DEVICE_SELECTOR="*:*.*.*" \ZEX_NUMBER_OF_CCS="0:4,1:4 ./a.out

注:







このプログラムは、FLAT モードの例 – OpenMP* のプログラムと同一です。唯一の違いは、プログラムを実行する前に追加の環境変数 (ONEAPI_DEVICE_SELECTOR および ZEX_NUMBER_OF_CCS) が設定され、CCS (スタックではなく) がデバイスとして公開されることです。
ONEAPI_DEVICE_SELECTOR=”:.*.*” を設定すると、CCS がルートデバイスとしてアプリケーションに公開されます。 LIBOMPTARGET_DEVICES=SUBSUBDEVICE を設定することもできます。
ZEX_NUMBER_OF_CCS=”0:4,1:4” は、スタック 0 の 4 つの CCS とスタック 1 の 4 つの CCS が公開されることを指定します。
OMP_TARGET_OFFLOAD=MANDATORY は、ターゲット領域が GPU 上で実行されることを確認するのに使用されます。GPU が見つからない場合、プログラムは失敗します。
FLAT モードがデフォルトであるため、実行コマンドで ZE_FLAT_DEVICE_HIERARCHY=FLAT を指定する必
要はありません。

単一 GPU カード (合計 2 つのスタック) を搭載したシステムで実行する場合:
libomptarget.so デバッグ情報を取得するには、実行コマンドに LIBOMPTARGET_DEBUG=1 を追加します。
$ OMP_TARGET_OFFLOAD=MANDATORY ONEAPI_DEVICE_SELECTOR="*:*.*.*" \ZEX_NUMBER_OF_CCS="0:4,1:4 LIBOMPTARGET_DEBUG=1 ./a.out >& libomptarget_debug.log

libomptarget_debug.log には次のように出力され、8 つの CCS (2 つのスタックそれぞれに 4 つの CCS) に対応する 8 つのデバイスが見つかったことが示されています。
Target LEVEL_ZERO RTL --> Found a GPU device, Name = Intel(R) Data Center GPU Max 1550
Target LEVEL_ZERO RTL --> Found 8 root devices, 8 total devices.

338

oneAPI GPU 最適化ガイド
Target LEVEL_ZERO RTL --> List of devices (DeviceID[.SubID[.CCSID]])
Target LEVEL_ZERO RTL --> -- 0.0.0
Target LEVEL_ZERO RTL --> -- 0.0.1
Target LEVEL_ZERO RTL --> -- 0.0.2
Target LEVEL_ZERO RTL --> -- 0.0.3
Target LEVEL_ZERO RTL --> -- 1.0.0
Target LEVEL_ZERO RTL --> -- 1.0.1
Target LEVEL_ZERO RTL --> -- 1.0.2
Target LEVEL_ZERO RTL --> -- 1.0.3

4 枚の GPU カード (合計 8 スタック) を搭載したシステムで実行する場合:
libomptarget.so デバッグ情報を取得するには、実行コマンドに LIBOMPTARGET_DEBUG=1 を追加します。
$ OMP_TARGET_OFFLOAD=MANDATORY LIBOMPTARGET_DEBUG=1 ./a.out >& libomptarget_debug.log

libomptarget_debug.log には次のように出力され、32 個の CCS (8 つのスタックそれぞれに 4 つの CCS) に対応する 32 個のデバイスが見つかったことが示されています。
Target LEVEL_ZERO RTL --> Found a GPU device, Name = Intel(R) Data Center GPU Max 1550
Target LEVEL_ZERO RTL --> Found 32 root devices, 32 total devices.
Target LEVEL_ZERO RTL --> List of devices (DeviceID[.SubID[.CCSID]])
Target LEVEL_ZERO RTL --> -- 0.0.0
Target LEVEL_ZERO RTL --> -- 0.0.1
Target LEVEL_ZERO RTL --> -- 0.0.2
Target LEVEL_ZERO RTL --> -- 0.0.3
Target LEVEL_ZERO RTL --> -- 1.0.0
Target LEVEL_ZERO RTL --> -- 1.0.1
Target LEVEL_ZERO RTL --> -- 1.0.2
Target LEVEL_ZERO RTL --> -- 1.0.3
Target LEVEL_ZERO RTL --> -- 2.0.0
Target LEVEL_ZERO RTL --> -- 2.0.1
Target LEVEL_ZERO RTL --> -- 2.0.2
Target LEVEL_ZERO RTL --> -- 2.0.3
Target LEVEL_ZERO RTL --> -- 3.0.0
Target LEVEL_ZERO RTL --> -- 3.0.1
Target LEVEL_ZERO RTL --> -- 3.0.2
Target LEVEL_ZERO RTL --> -- 3.0.3
Target LEVEL_ZERO RTL --> -- 4.0.0
Target LEVEL_ZERO RTL --> -- 4.0.1
Target LEVEL_ZERO RTL --> -- 4.0.2
Target LEVEL_ZERO RTL --> -- 4.0.3
Target LEVEL_ZERO RTL --> -- 5.0.0
Target LEVEL_ZERO RTL --> -- 5.0.1
Target LEVEL_ZERO RTL --> -- 5.0.2
Target LEVEL_ZERO RTL --> -- 5.0.3
Target LEVEL_ZERO RTL --> -- 6.0.0
Target LEVEL_ZERO RTL --> -- 6.0.1
Target LEVEL_ZERO RTL --> -- 6.0.2
Target LEVEL_ZERO RTL --> -- 6.0.3
Target LEVEL_ZERO RTL --> -- 7.0.0
Target LEVEL_ZERO RTL --> -- 7.0.1
Target LEVEL_ZERO RTL --> -- 7.0.2
Target LEVEL_ZERO RTL --> -- 7.0.3

MPI で複数の CCS を使用する
GPU スタックごとに 1 つ以上の CCS を実行する一般例は、CPU で実行されるオフロードされていないコードによってアプリケーション時間の大部分が消費される MPI アプリケーションです。4 CCS モードで実行すると、ユーザーは
GPU スタックの数の 4 倍の MPI ランクで実行できるため、ホストプロセスはより多くの CPU コアを消費できます。
MPI を介して実行される DGEMM の例を次のソースに示します。
339

oneAPI GPU 最適化ガイド#include "mkl.h"#include "mkl_omp_offload.h"#include#include <chrono>#include <limits>#include <mpi.h>#include <omp.h>#define FLOAT double#define MPI_FLOAT_T MPI_DOUBLE#define MKL_INT_T MKL_INT#define index(i, j, ld) (((j) * (ld)) + (i))#define RAND() ((FLOAT)rand() / (FLOAT)RAND_MAX * 2.0 - 1.0)
#define LD_ALIGN 256#define LD_BIAS 8#define HPL_PTR(ptr_, al_) ((((size_t)(ptr_) + (al_)-1) / (al_)) * (al_))static inline MKL_INT_T getld(MKL_INT_T x) {MKL_INT_T ld;
ld = HPL_PTR(x, LD_ALIGN);
if (ld - LD_BIAS >= x)
ld -= LD_BIAS;else
ld += LD_BIAS;
return ld;}
int main(int argc, char **argv) {if ((argc < 4) || (argc > 4 && argc < 8)) {printf("Performs a DGEMM test C = alpha*A*B + beta*C\n");printf("A matrix is MxK and B matrix is KxN\n");printf("All matrices are stored in column-major format\n");printf("Run as ./dgemm <M> <K> <N> [ <beta> <iterations>]\n");printf("Required inputs are:\n");printf("
M: number of rows of matrix A\n");printf("
K: number of cols of matrix A\n");printf("
N: number of cols of matrix B\n");printf("Optional inputs are (all must be provided if providing any):\n");printf("alpha: scalar multiplier (default: 1.0)\n");printf("beta: scalar multiplier (default: 0.0)\n");printf("iterations: number of blocking DGEMM calls to perform ""(default: 10)\n");return EXIT_FAILURE;}
MKL_INT_T HA = (MKL_INT_T)(atoi(argv[1]));
MKL_INT_T WA = (MKL_INT_T)(atoi(argv[2]));
MKL_INT_T WB = (MKL_INT_T)(atoi(argv[3]));
FLOAT alpha, beta;
int niter;if (argc > 4) {sscanf(argv[4], "%lf", &alpha);sscanf(argv[5], "%lf", &beta);
niter = atoi(argv[6]);} else {alpha = 1.0;
beta = 0.0;
niter = 10;}
MKL_INT_T HB = WA;
MKL_INT_T WC = WB;
MKL_INT_T HC = HA;
MKL_INT_T ldA = getld(HA);
MKL_INT_T ldB = getld(HB);
MKL_INT_T ldC = getld(HC);double tot_t = 0.0, best_t = std::numeric_limits<double>::max();
FLOAT *A = new FLOAT[ldA * WA];FLOAT *B, *C, *local_B, *local_C;
MPI_Init(&argc, &argv);
int mpi_rank, mpi_size;
MPI_Comm_size(MPI_COMM_WORLD, &mpi_size);
MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank);
if (mpi_rank == 0) {340

oneAPI GPU 最適化ガイド
B = new FLOAT[ldB * WB];
C = new FLOAT[ldC * WC];
srand(2864);
for (int j = 0; j < WA; j++)for (int i = 0; i < HA; i++)A[index(i, j, ldA)] = RAND();
for (int j = 0; j < WB; j++)for (int i = 0; i < HB; i++)B[index(i, j, ldB)] = RAND();
if (beta != 0.0) {for (int j = 0; j < WC; j++)for (int i = 0; i < HC; i++)C[index(i, j, ldC)] = RAND();} else {for (int j = 0; j < WC; j++)for (int i = 0; i < HC; i++)C[index(i, j, ldC)] = 0.0;}}
size_t sizea = (size_t)ldA * WA;
size_t local_sizeb, local_sizec;
int *displacements_b = new int[mpi_size];
int *send_counts_b = new int[mpi_size];
int *displacements_c = new int[mpi_size];
int *send_counts_c = new int[mpi_size];
int local_WB = WB / mpi_size;
send_counts_b[0] = ldB * (local_WB + WB % mpi_size);
send_counts_c[0] = ldC * (local_WB + WB % mpi_size);displacements_b[0] = 0;
displacements_c[0] = 0;
for (int i = 1; i < mpi_size; i++) {send_counts_b[i] = ldB * local_WB;
send_counts_c[i] = ldC * local_WB;
displacements_b[i] = displacements_b[i - 1] + send_counts_b[i - 1];
displacements_c[i] = displacements_b[i - 1] + send_counts_c[i - 1];}
if (mpi_rank == 0) {local_WB += WB % mpi_size;}
local_sizeb = ldB * local_WB;
local_sizec = ldC * local_WB;
local_B = new FLOAT[local_sizeb];local_C = new FLOAT[local_sizec];
MPI_Bcast(A, sizea, MPI_FLOAT_T, 0, MPI_COMM_WORLD);MPI_Scatterv(B, send_counts_b, displacements_b, MPI_FLOAT_T, local_B,
local_sizeb, MPI_FLOAT_T, 0, MPI_COMM_WORLD);MPI_Scatterv(C, send_counts_c, displacements_c, MPI_FLOAT_T, local_C,
local_sizec, MPI_FLOAT_T, 0, MPI_COMM_WORLD);#if defined(OMP_AFFINITIZATION)#if OMP_AFFINITIZATION == 1
int ndev = omp_get_num_devices();
int dnum = mpi_rank % ndev;
omp_set_default_device(dnum);#endif#endif#pragma omp target data map(to\: A [0:sizea], local_B [0:local_sizeb])\map(tofrom\: local_C [0:local_sizec]){#pragma omp dispatch
dgemm("N", "N", &HA, &local_WB, &WA, &alpha, A, &ldA, local_B, &ldB, &beta,
local_C, &ldC);
for (int i = 0; i < niter; i++) {auto start_t = std::chrono::high_resolution_clock::now();#pragma omp dispatch
dgemm("N", "N", &HA, &local_WB, &WA, &alpha, A, &ldA, local_B, &ldB,&beta, local_C, &ldC);
MPI_Barrier(MPI_COMM_WORLD);

341

oneAPI GPU 最適化ガイド
auto end_t = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end_t - start_t;
tot_t += diff.count();
best_t = std::min(best_t, diff.count());}}
MPI_Gatherv(local_C, local_sizec, MPI_FLOAT_T, C, send_counts_c,
displacements_c, MPI_FLOAT_T, 0, MPI_COMM_WORLD);
delete[] local_B;
delete[] local_C;
delete[] displacements_b;
delete[] displacements_c;delete[] send_counts_b;
delete[] send_counts_c;
MPI_Allreduce(MPI_IN_PLACE, &tot_t, 1, MPI_FLOAT_T, MPI_MAX, MPI_COMM_WORLD);MPI_Allreduce(MPI_IN_PLACE, &best_t, 1, MPI_FLOAT_T, MPI_MAX, MPI_COMM_WORLD);
if (mpi_rank == 0) {double tflop_count = (double)2.0 * HA * WB * WA;
if (beta != 0.0)
tflop_count += (double)HA * WB;
tflop_count *= 1.E-12;
printf("Total runtime for %d iterations: %f seconds.\n", niter, tot_t);
printf("Mean TFLOP/s: %f\n", (double)niter * tflop_count / tot_t);
printf("Best TFLOP/s: %f\n", (double)tflop_count / best_t);delete[] B;
delete[] C;}delete[] A;
MPI_Finalize();
return EXIT_SUCCESS;}

この例の DGEMM は OpenMP* オフロードを通じて実行される MKL 呼び出しです。行列は MPI ランク間で静的に分割されます。
バイナリーをビルドするには、次のコマンドを実行します。
$ cd examples/MPI/02_omp_mpi_onemkl_dgemm$ makeインテル® MPI では、各プロセスは 1 つまたは複数の GPU スタックにバインドできます。GPU スタックに複数のプロ
セスが割り当てられている場合、GPU ドライバーはスタックに関連付けられた CCS の 1 つにカーネルをエンキュー
します。使用するデバイス スタックを指定するには、環境変数 I_MPI_OFFLOAD_CELL_LIST を使用します。
たとえば、4 つの MPI ランクを最初のデバイスの最初のスタックに割り当てて、アプリケーションを 4 CCS モードで実行するには、次のようにします。
$ export ZEX_NUMBER_OF_CCS=0:4$ export I_MPI_OFFLOAD_CELL_LIST=0,0,0,0$ mpirun -n 4 ./dgemm 8192 8192 8192

最初の 4 つの MPI ランクを最初のデバイスの最初のスタックに割り当て、次の 4 つの MPI ランクを最初のデバイスの 2 番目のスタックに割り当ててアプリケーションを実行します。この場合、前回の実行の 2 倍の FLOP/s が得られることが期待されます。
$ export ZEX_NUMBER_OF_CCS=0:4,1:4$ export I_MPI_OFFLOAD_CELL_LIST=0,0,0,0,1,1,1,1$ mpirun -n 8 ./dgemm 8192 8192 8192

次のインテル® MPI 環境変数はデフォルトですが、状況に応じて指定または変更すると便利な場合があることに注意
してください。
342

oneAPI GPU 最適化ガイド$ export I_MPI_OFFLOAD_CELL=tile # GPU タイルに関連付けられた MPI ランク$ export I_MPI_OFFLOAD=1 # デバイスポインターを使用した MPI 作業を有効にする$ export I_MPI_OFFLOAD_TOPOLIB=level_zero # トポロジー検出にはレベル 0 を使用 （GPU ピンニング）

MPICH では、MPI ランクは環境変数 ZE_AFFINITY_MASK を通じて GPU スタックに明示的に関連付けられます。
その後、ドライバーはランクをスタック上の CCS に関連付けます。
適切な MPICH インストールがロードされている場合、同じサンプル・アプリケーションを MPICH で構築できます。同様に MPI ランクをバインドする次のようなサンプルスクリプトが提供されています。
#!/bin/bash
if [ -z ${NCCS} ]; then
NCCS=1fi
if [ -z ${NGPUS} ]; then
NGPU=1fi
if [ -z ${NSTACKS} ]; then
NSTACK=1
fi
subdevices=$((NGPU*NSTACK))
export ZE_AFFINITY_MASK=$(((MPI_LOCALRANKID/NCCS)%subdevices))echo MPI_LOCALRANKID = $MPI_LOCALRANKID
exec $@

ZE_AFFINITY_MASK = $ZE_AFFINITY_MASK

ノードに 6 つの GPU があり、各 GPU に 2 つのスタックがあり、スタックごとに 4 つの CCS で実行する場合、このス
クリプトの使用方法は次のようになります。
$ export NGPUS=6$ export NSTACKS=2$ export NCCS=4$ export ZEX_NUMBER_OF_CCS=0:${NCCS},1:${NCCS}$ mpiexec -n 48 ./gpu_rank_bind.sh ./dgemm 8192 8192 8192

MKL DGEMM は OpenMP* オフロードを通じて処理されるため、OpenMP* を通じて MPI ランクを特定の CCS に明示的に関連付けることもできます。一部の MPI/OpenMP* オフロード・アプリケーションでは、この方針が使用されます。
#if OMP_AFFINITIZATION == 1
int ndev = omp_get_num_devices();dnum = mpi_rank % ndev;
omp_set_default_device(dnum);#endif...
#pragma omp target data map(to:A[0:sizea],local_B[0:local_sizeb])map(tofrom:local_C[0:local_sizec]){...
#pragma omp dispatch
dgemm("N","N",&HA,&local_WB,&WA,&alpha,A,&ldA,local_B,&ldB,&beta,local_C,&ldC);...
}

このモードのバイナリーは、次のオプションでビルドできます。
343

oneAPI GPU 最適化ガイド$ cd examples/MPI/02_omp_mpi_onemkl_dgemm$ make OMP_AFFINITIZATION=1

その後、デバイスのアフィニティーを明示的に指定せずにアプリケーションを実行できます。
$ export ZEX_NUMBER_OF_CCS=0:4,1:4$ export LIBOMPTARGET_DEVICES=SUBSUBDEVICE$ mpirun -n 8 ./dgemm.out

5-16-8

用語

用語
ブリッター
子スレッド

略語
BLT

コマンド

コマンド・ストリーマー

CS または CSI

コア
デュアルサブスライス

DSS

最後のスレッド

EOT

例外

実行チャンネル
実行サイズ

ExecSize

実行ユニットEU

実行ユニット識別子

EUID

実行幅

ExecWidth344

定義
ブロックイメージ転送。
別のスレッドで作成された分岐ノードまたはリーフノードのスレッド。これはメディア固定機能パイプラインに関連付けられたスレッドの一種です。子スレッドは、VE で実行される (親) ス
レッドから生成され、TS ユニットからスレッド・ディスパッ
チャーに転送されます。子スレッドは、ブランチノードのスレッドであるか、リーフノードのスレッドであるかにより、子スレッドを持つ場合と持たない場合があります。子スレッドの URBやスクラッチメモリーなど、事前に割り当てられたリソースは親スレッドで管理されます。「親スレッド」を参照してください。
コマンド・ストリーマーによってメモリー内のリングバッファーからフェッチされ、パイプラインにルーティングされるディレク
ティブ。命令キャッシュ・サブシステムによってフェッチされ、VEで実行される命令とは異なります。
コマンドをフェッチして解析し、適切なパイプラインにルーティ
ングするグラフィックス処理エンジンの機能ユニット。
マルチプロセッサー・システムにおける VE の別名。「EU」を参照。
サンプラー、データポート、ピクセルポートなどの共有機能ユニットの共通セットを持つベクトルエンジン (VE)、またはベク
トルエンジン (ＶＥ) の集合。
メッセージを要求するスレッドが終了したことを示す、出力
メッセージバス上のメッセージ側波信号。スレッドが正しく終了するには、メッセージ記述フィールドの EOT ビットが指定された SEND 命令が最低 1 つ必要です。
スレッド命令の VE/EU 実行に対する中断タイプ。例外が発生すると、VE/EU スレッドは例外を処理するシステムルーチンを実行します。
SIMD オペランドの単一レーン。
SIMD 命令で処理されるデータ要素数。これは、命令フィール
ドの 1 つであり、命令ごとに変更できます。
マルチプロセッサー・システム内のマルチスレッド・プロセッサー。各 EU は、命令フェッチとデコード、レジスターファイル、
ソースオペランドのスウィズル、SIMD ALU などを含む完全な機能を備えたプロセッサーであり、EU はコアとも呼ばれます。
スレッド状態レジスター (SR0) 内の 4 ビットのフィールド。ス
レッドが配置されている EU の行と列を識別します。スレッド
は EUID と TID によって一意に識別できます。
単一の SIMD 命令が処理できるデータ要素の幅。

oneAPI GPU 最適化ガイド
用語
汎用レジスターファイル

略語
GRF

汎用状態ベースアドレス

グラフィックス処理エンジン

GPE

メモリーマップド入出力
メッセージ

MMIO

親スレッド

リソース・ストリーマー

RS

ルートスレッド

単一命令複数データ

スポーン
サブレジスター

サブシステム
サーフェス
サーフェス状態
サーフェス状態ベースポイン
ター
同期されたルートスレッド

SIMD

定義
オペランドのソースとデスティネーション向けにすべての
EU/VE で共有され読み取り/書き込み可能なラージ・レジス
ター・ファイル。これは、256 ビット・レジスターの配列として編成された、最も汎用的に使用される読み取り/書き込みレジス
ター空間です。
メモリーに常駐する「状態データ」ブロックのグラフィックス・
アドレス、状態ブロック、スクラッチベース、定数バッファー、
カーネルプログラムが含まれます。メモリーブロックの内容
は、汎用状態ベース・アドレス・レジスターの内容からのオフ
セットで参照されます。「グラフィックス処理エンジン」を参照
してください。
サブシステム、3D およびメディア・パイプライン、コマンド・ス
トリーマーの総称。
CPU/GPU と周辺機器との間で入出力を行う方法。
スレッドから別のスレッド、別の共有変数、または別の固定関数に送信されるデータパッケージ。メッセージパッシングは、インテル® GPU アーキテクチャーの主要な通信メカニズムです。
スレッド生成階層におけるルートノード、または分岐ノードに対応するスレッド。親スレッドは、スレッド生成階層内の位置に応じて、ルートスレッドまたは子スレッドになります。
グラフィックス処理エンジンの機能ユニット。リングバッ
ファー内のコマンドを調査し、グラフィックス処理に残された、長いレイテンシーの項目の前処理を試みます。
ルートノードのスレッド。スレッド生成階層のルートノードに対応するスレッド。これはメディア固定機能パイプラインに関連付けられたスレッドの一種です。ルートスレッドは、VFE ユニットから生成され、TS ユニットからスレッド・ディスパッチャーに転送されます。ルートスレッドは、子スレッドを持つ場合と持たない場合があります。ルートスレッドは、TS によって管理されるスクラッチメモリーを持つ場合があります。子スレッドを持つルートスレッドの URB のリソースは、VFE によって管理されます。
命令レベルでのデータ並列処理を利用する並列処理アーキテ
クチャー。また、このようなアーキテクチャーの命令を記述したり、特定の命令 (SIMD8 など） のデータ並列処理を記述できます。
EU/VE で実行するスレッドを開始します。３D パイプラインのFF ユニットと同様に、スレッドのスポーンで実行されます。
SIMD レジスターのサブフィールド。レジスターファイルまたはレジスタータイプ向けにアラインされた固定サイズのレジスターです。例えば、GRF レジスター r2 は、256 ビット幅で、256 ビットにアラインされたレジスターです。サブレジスター
r2.3:d は、GRF レジスター r2 の 4 つ目の dword です。
共有機能や EU/VE など、FF ユニットが共有するリソースの名称。
テクスチャー、バッファー、レンダーターゲットなどのレンダリ
ング・オペランドやデスティネーション。
レンダーサーフェスに関連付けられた状態。
バインドテーブルおよびサーフェス状態データを参照する際に使用するベースアドレス。
「ルートスレッドのディスパッチ」メッセージに応じて、TS から
ディスパッチされるルートスレッド。

345

oneAPI GPU 最適化ガイド
用語システム IP

略語
SIP

システムルーチンスレッド

スレッド・ディスパッチャー

TD、TDL

スレッド識別子

TID

スレッドペイロード

スレッド起動同期されないルートスレッド

346

TS

定義
すべてのスレッドに対して 1 つのグローバルシステム IP レジ
スターがある。スレッドから見ると、これは読み取り専用の仮想レジスターです。例外が発生すると、ハードウェアは何らかの処理を行ってから SIP にジャンプします。
例外を処理する命令シーケンス。SIP はこのルーチンに関連付けられてプログラムされており、例外が発生したすべてのス
レッドはこのルーチンを呼び出します。
EU/VE 上のカーネルプログラムで実行されたインスタンス。
スレッドのライフサイクルは、スレッド・ディスパッチャーから
EU/VE にディスパッチされた後、最初の命令から始まり、最後の命令 (スレッドの終了を示す EOT 送信命令) の実行まで続きます。システム内のスレッドは、互いに独立することも、メッ
セージ・ゲートウェイの共有機能により相互に通信することもできます。
固定機能ユニットからのスレッド開始要求を調停し、EU/VE でスレッドをインスタンス化する機能ユニット。
スレッドが EU/VE 上のいずれのスレッドスロットを占有するか識別する、スレッド状態レジスター (SR0) のフィールド。ス
レッドは EUID と TID によって一意に識別できます。
スレッドが実行を開始する前に、データがスレッドの GRF (r0から) に事前ロードされます。このデータは通常、スポーン・エ
ンティティー (FF ユニット) によって提供される制御情報とURB から読み取られたデータの組み合わせです。
メディア・パイプラインの 2 番目で、最後の固定関数ステージ。
汎用/メディア処理に変わって新しいスレッドを開始します。
TS によって自動的にディスパッチされるルートスレッド。

oneAPI GPU 最適化ガイド

5-17 レベルゼロ
レベルゼロは、oneAPI ライブラリーがターゲットデバイスのハードウェアの機能を活用できるようにする、低レベルのインターフェイスです。この節では、oneAPI レベルゼロのインテル® グラフィックス計算ランタイムで採用されているアーキテクチャーの設計について説明します。実装の詳細と最適化のガイドライン、およびサポートされるプラット
フォームで利用可能なさまざまな機能について説明します。
•

即時コマンドリスト

5-17-1
5-17-1-1

即時コマンドリスト
はじめに

即時コマンドは、低レイテンシーの送信モデルを可能にする、レベルゼロ仕様で提供される機能です。このスキームでは、カーネルの起動やメモリーコピーなど、コマンドリストに追加されたコマンドを実行するため直ちにデバイスに送信します。これは、複数のコマンドを結合して同時に送信する通常のコマンドリストとは異なります。
通常のコマンドリストと比較すると、即時コマンドリストには次のような特徴があります。
•

即時コマンドリストは暗黙のコマンドキューであり、コマンドキュー記述子を使用して作成されます。

•

即時コマンドリストに追加されたコマンドは、送信されるとデバイス上ですぐに実行されます。

•

即時コマンドリストは、クローズしたり、リセットする必要はありません。

•

即時コマンドリストに関連付けられたコマンド・キュー・ハンドルがないため、zeCommandQueueSynchronize や
zeFenceHostSynchronize を介して即時コマンドリストを同期することはできません。即時コマンドリストに送信されたコマンドが完了したことを確認するには、イベントを使用することを推奨します。

即時コマンドリストの意図は、デバイスに極めて軽量な送信インターフェイスを提供することであり、小規模または短
時間のカーネル起動を反復して複数回実行するようなワークロードに適しています。そのようなワークロードの例には、HPC 環境や ML/DL フレームワークなどがあげられます。

5-17-1-2

プログラミング・モデル

次のコードは、即時コマンドリストを作成し、カーネルを送信する方法を示します。同期は、イベント状態を確認することで行われます。
ze_command_queue_desc_t cmdQueueDesc = {ZE_STRUCTURE_TYPE_COMMAND_QUEUE_DESC};
cmdQueueDesc.pNext = nullptr;
cmdQueueDesc.flags = 0;
cmdQueueDesc.priority = ZE_COMMAND_QUEUE_PRIORITY_NORMAL;
cmdQueueDesc.ordinal = queueGroupOrdinal;
cmdQueueDesc.index = 0;
cmdQueueDesc.mode = ZE_COMMAND_QUEUE_MODE_ASYNCHRONOUS;
zeCommandListCreateImmediate(context, device, &cmdQueueDesc, &cmdList);
zeCommandListAppendLaunchKernel(cmdList, kernel, &dispatchTraits,
events[0], 0, nullptr);// 非同期モードでは、同期にイベントを使用
zeEventHostSynchronize(events[0], std::numeric_limits<uint64_t>::max() - 1);

347

oneAPI GPU 最適化ガイド
即時コマンド リストは、インオーダー・キューを実装するのに使用することもできます。この場合、リストに送信されたコマンドは、以下に示すように、イベントを使用してチェーンされます。
zeCommandListAppendMemoryCopy(cmdList, deviceBuffer, hostBuffer, allocSize,
events[0],0, nullptr);
zeCommandListAppendMemoryCopy(cmdList, stackBuffer, deviceBuffer, allocSize,
events[1],1,&events[0]);zeEventHostSynchronize(events[1], std::numeric_limits<uint64_t>::max() - 1));

通常のコマンドリストと同様に、即時コマンドリストも同期できます。この場合、同期は暗黙的に行われます。リストに送信されたそれぞれのコマンドはすぐに送信され、呼び出しから戻った時点で完了することが保証されます。
ze_command_queue_desc_t cmdQueueDesc = {ZE_STRUCTURE_TYPE_COMMAND_QUEUE_DESC};
cmdQueueDesc.pNext = nullptr;
cmdQueueDesc.flags = 0;
cmdQueueDesc.priority = ZE_COMMAND_QUEUE_PRIORITY_NORMAL;
cmdQueueDesc.ordinal = queueGroupOrdinal;
cmdQueueDesc.index = 0;
cmdQueueDesc.mode = ZE_COMMAND_QUEUE_MODE_SYNCHRONOUS;
zeCommandListCreateImmediate(context, device, &cmdQueueDesc, &cmdList);
zeCommandListAppendLaunchKernel(cmdList, kernel, &dispatchTraits,
nullptr, 0, nullptr);// この時点でカーネルは実行済み

その他のサンプルコードについては、https://github.com/intel/compute-benchmarks (英語) リポジトリーを 参 照 し て く だ さ い 。 こ れ か ら 始 め る 方 に は create_command_list_immediate_l0.cpp やexecute_command_list_immediate_l0.cpp などのシナリオが適しています。

5-18 パフォーマンスのプロファイルと解析システムの動作を理解することは、情報に基づいて最適化を選択する上で不可欠です。プロファイラー、アナライザー、
デバッガーなどのツールはそのための十分な機能を備えています。インターバル・タイマー、カーネルタイマー、
print 文などの機能は軽量ですが、最適化の過程で重要な役割を果たします。
以下の節では、ソフトウェアを最適化するためこれらのツールをどのように使用するかについて説明します。
•
•
•
•
•
•

348

GPU 向けのインテル® プロファイル・ツール・インターフェイスのパフォーマンス・ツール
タイマーを使用
インテル® VTune™ プロファイラー
インテル® Advisor
OpenCL* アプリケーション向けインテル® インターセプト・レイヤー
GPU 向けのインテル® プロファイル・ツール・インターフェイスのパフォーマンス・ツール

oneAPI GPU 最適化ガイド

5-18-1

タイマーを使用標準の C++ chrono ライブラリーは、SYCL* で各種精度の時間を追跡する際に利用できます。次の例は、chrono タ
イマークラスを使用して、ホストからカーネルの実行時間を計測する方法を示します。
1. #include <CL/sycl.hpp>
2. #include <iostream>
3. using namespace sycl;
4.
5. // この例の配列タイプとデータサイズ
6. constexpr size_t array_size = (1 << 16);
7. typedef std::array<int, array_size> IntArray;8.
9. double VectorAdd(queue &q, const IntArray &a, const IntArray &b, IntArray &sum) {10.
range<1> num_items{a.size()};11.
12.
buffer a_buf(a);13.
buffer b_buf(b);14.
buffer sum_buf(sum.data(), num_items);
15.
16.
auto t1 = std::chrono::steady_clock::now();// 開始時間
17.
18.
q.submit([&](handler &h) {19.
// 入力アクセサー
20.
auto a_acc = a_buf.get_access<access::mode::read>(h);21.
auto b_acc = b_buf.get_access<access::mode::read>(h);22.
23.
// 出力アクセサー
24.
auto sum_acc = sum_buf.get_access<access::mode::write>(h);25.
26.
h.parallel_for(num_items, [=](id<1> i) { sum_acc[i] = a_acc[i] +b_acc[i]; });27.
}).wait();
28.
29.
auto t2 = std::chrono::steady_clock::now();// 終了時間30.
31.
return(std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count());
32. }
33.
34. void InitializeArray(IntArray &a) {35.
for (size_t i = 0; i < a.size(); i++) a[i] = i;
36. }
37.
38. int main() {39.
default_selector d_selector;
40.
41.
IntArray a, b, sum;42.
43.
InitializeArray(a);44.
InitializeArray(b);45.
46.
queue q(d_selector);
47.
48.
std::cout << "Running on device: "49.
<< q.get_device().get_info<info::device::name>() << "\n";50.
std::cout << "Vector size: " << a.size() << "\n";51.
52.
double t = VectorAdd(q, a, b, sum);
53.
54.
std::cout << "Vector add successfully completed on device in " << t << "microseconds\n";55.
return 0;
56. }

349

oneAPI GPU 最適化ガイド
このタイミングは純粋にホスト側のものであることに注意してください。デバイス上のカーネルの実行は、ホストがカーネルを送信した後、かなり遅れて開始されることがあります。SYCL* は、カーネルの実行に消費した時間を追跡す
るプロファイル機能を提供します。
1. #include <CL/sycl.hpp>
2. #include <array>
3. #include <iostream>
4. using namespace sycl;
5.
6. // この例の配列タイプとデータサイズ
7. constexpr size_t array_size = (1 << 16);
8. typedef std::array<int, array_size> IntArray;
9.
10. double VectorAdd(queue &q, const IntArray &a, const IntArray &b, IntArray &sum) {11.
range<1> num_items{a.size()};12.
13.
buffer a_buf(a);14.
buffer b_buf(b);15.
buffer sum_buf(sum.data(), num_items);
16.
17.
event e = q.submit([&](handler &h) {18.
// 入力アクセサー
19.
auto a_acc = a_buf.get_access<access::mode::read>(h);20.
auto b_acc = b_buf.get_access<access::mode::read>(h);21.
22.
// 出力アクセサー
23.
auto sum_acc = sum_buf.get_access<access::mode::write>(h);24.
25.
h.parallel_for(num_items, [=](id<1> i) { sum_acc[i] = a_acc[i] +b_acc[i]; });
26.
});
27.
q.wait();28.
return(e.template get_profiling_info<info::event_profiling::command_end>() 29.
e.template get_profiling_info<info::event_profiling::command_start>());
30. }
31.
32. void InitializeArray(IntArray &a) {33.
for (size_t i = 0; i < a.size(); i++) a[i] = i;
34. }
35.
36. int main() {37.
default_selector d_selector;
38.
39.
IntArray a, b, sum;40.
41.
InitializeArray(a);42.
InitializeArray(b);43.
44.
queue q(d_selector, property::queue::enable_profiling{});45.
46.
std::cout << "Running on device: "47.
<< q.get_device().get_info<info::device::name>() << "\n";48.
std::cout << "Vector size: " << a.size() << "\n";49.
50.
double t = VectorAdd(q, a, b, sum);51.
52.
std::cout << "Vector add successfully completed on device in " << t << "nanoseconds\n";53.
return 0;
54. }この例を実行すると、chrono タイマーを使用する時間が SYCL* プロファイル・クラスを使用する時間よりもかなり長くなる可能性があります。これは、SYCL* プロファイルにはホストとオフロードデバイス間のデータ転送時間が含まれないためです。
350

oneAPI GPU 最適化ガイド

5-18-2インテル® VTune™ プロファイラー

インテル® VTune™ プロファイラーは、CPU、GPU、その他のアクセラレーター・アプリケーションを最適化するためのパフォーマンス解析ツールです。開発者は、このツールを使用することで、アプリケーションのパフォーマンス・ボトル
ネックを見つけ、利用可能なハードウェア・リソースを最大限に活用するためにアプリケーションを最適化できる場所と方法を特定できます。GPU パフォーマンスに関しては、次のことが可能です。
•
•
•
•
•
•
•
•
•
•

アプリケーションが CPU または GPU に依存しているか調査
ハードウェア使用率を計算
XVE ストールをレポートする
インテル® データセンター GPU マックス・シリーズでストールの場所とストールの種類をレポート
さらに最適化の候補となるホットな計算タスクまたはカーネルを特定データ転送と帯域幅の利用率を解析
パフォーマンスのボトルネックとなるメモリー/キャッシュユニットを特定
アルゴリズム内の非効率的なデータ・アクセス・パターンと、重大なストールの原因となるメモリーアクセスを検出コンパイラーによって生成された SIMD 命令の効率を判断
アプリケーション・パフォーマンス・スナップショットを使用して、複数の GPU から収集されたデータに関する迅
速な情報を提供インテル® VTune™ プロファイラーを使用した GPU パフォーマンスの最適化の詳細とテクニックについては、以下を参照してください。
インテル® GPU 向けのソフトウェア最適化

インテル® VTune™ プロファイラーを使用してインテル® GPU 向けにアプリケーションを最適化

5-18-3インテル® Advisor

インテル® Advisor は、開発者が高性能なアプリケーションを設計および最適化するのを支援するツールです。GPU
パフォーマンスに関しては、次の機能を提供します。
•

オフロードのモデル化•

GPU ルーフラインの調査機能

オフロードのモデル化は、境界とボトルネックのパフォーマンス・モデルを使用して、上限の高速化の推定値を生成します。CPU メトリックとアプリケーション特性を入力として受け取り、解析モデルを適用して、ターゲット GPU での実
行時間と特性を推定します。次のことが可能です。






オフロードのメリットがあるコード領域を特定する
パフォーマンスのボトルネックを見つける
オフロードによるプロジェクト・パフォーマンスの向上
既存のコードを次世代 GPU で実行するとプロジェクトのパフォーマンスが向上する
最適化ガイドを提供する

ルーフラインとパフォーマンスの調査は、GPU カーネルの実際のパフォーマンスとハードウェア固有のパフォーマン
ス上限を視覚化し、マルチタイル/マルチカード GPU システムで顕著なパフォーマンスの制限要因を特定します。開発者は、次のことを理解できます。

351

oneAPI GPU 最適化ガイド

現在のハードウェア・リソースで達成可能な最大のパフォーマンス
現在の実装が現在のハードウェア上で最適に動作するかどうか
現在の実装が最適でない場合、最適化するのに最適な候補
メモリー帯域幅、各候補の計算能力などのパフォーマンスを制限する要因

インテル® Advisor を使用して GPU パフォーマンスを最適化する方法の詳細とテクニックについては、インテル®
Advisor パフォーマンス最適化クックブックを参照してください。

5-18-4OpenCL* アプリケーション向けインテル® インターセプト・レイヤー

OpenCL* アプリケーション用のインターセプト・レイヤーは、OpenCL* アプリケーションをトレースおよび解析する
コマンドライン・ツールです。ホストとデバイスのアクティビティーのログ記録、カーネルバイナリーのダンプと逆アセンブルなどの機能を備えています。oneAPI アプリケーションで OpenCL* バックエンドを使用している場合、このツールはパフォーマンス・チューニングに役立つことがあります。
ソースコードを含む詳細情報は、github リポジトリーの Intercept Layer for OpenCL Applications (英語) で入手できます。
必要条件: インテル® Advisor を使用するには、次の環境変数を設定します。
••

Linux*: source <install-dir>/advisor-vars.sh
Windows*: <advisor-install-dir>\advisor-vars.bat

この章の残りのセクションでは、上記の 2 つの機能と GPU ルーフラインの調査を使用して、メモリー依存のアプリ
ケーションを解析および最適化する方法について詳しく説明します。

5-18-5

GPU 向けのインテル® プロファイル・ツール・インターフェイスのパ
フォーマンス・ツール

GPU 向けのインテル® プロファイル・ツール・インターフェイスには、ツールフォルダー内にインテル® oneAPI アプリ
ケーション用のトレースおよびプロファイル・ツールのセットが含まれています。これらの軽量なコマンドライン・ツー
ルは、レベルゼロと OpenCL* バックエンドの両方をサポートします。
デバイス・アクティビティーに加えて、ツールはレベル 0 または OpenCL* ランタイムレイヤーから MPI レイヤーまでのホスト・アクティビティーをトレースできます。ツールを使用すると、デバイスとホストのボトルネックをすばやく特定できます。また、スレッド占有率、メモリー・トラフィック、キャッシュ使用率、関数ユニット使用率などの GPU ハード
ウェア・パフォーマンス・カウンターをプロファイルすることもできます。さらに、unitrace ツールを実行して命令レ
ベルで GPU カーネルをプロファイルし、実行ユニットまたはベクトルエンジンをストールさせる命令を特定して、ストールの理由を報告できます。
JSON 形式で生成されたパフォーマンス・データは、https://ui.perfetto.dev (英語) などのトレースビューアーを使用してブラウザーで表示できます。
各ツールの詳細情報とソースコードについては、ツールフォルダーを確認してください。

352

oneAPI GPU 最適化ガイド

5-19 GPU デバイスの構成
5-19-1

はじめに

アプリケーションから最高のエンドツーエンドのパフォーマンスを引き出すには、システムレベルでデバイス構成を変更することが必要な場合もあります。これらの変更は、sudo アクセスが必要かどうかに応じて、システム管理者が行うことが求められることがあります。
この節では、Linux* 環境でパフォーマンスを得るため、特定のデバイス向けの構成を変更するガイドラインと手順を説明します。

5-19-2

GPU ドライバーまたはプラグインをインストール (オプション)

C++ と SYCL* を使用して、インテル、AMD*、または NVIDIA* GPU で実行される oneAPI アプリケーションを開発できます。
特定の GPU 用のアプリケーションを開発して実行するには、対応するドライバーまたはプラグインをインストールする必要があります。
インテル® GPU を使用するには、最新のインテル® GPU ドライバーをインストール (英語) してください。
インテル® oneAPI DPC++ コンパイラーで AMD* GPU を使用するには、Codeplay から AMD GPU 用 oneAPI
プラグインをインストールしてください。
インテル® oneAPI DPC++ コンパイラーで NVIDIA* GPU を使用するには、Codeplay から NVIDIA GPU 用
oneAPI プラグインをインストールしてください。

5-19-3

GPU 周波数を固定することのパフォーマンスへの影響

アプリケーションが計算のほとんどに GPU を使用する場合、最適な周波数に GPU を固定することを推奨します。このようなアプリケーションの例として、デバイスにオフロードされたアルゴリズムが計算集約型のコードを持つハイパ
フォーマンス・コンピューティングのワークロードがあります。
カーネルの実行時間が短くても、データ交換に多くの時間を費やすメモリー依存型のアプリケーションでは、GPU を
高い周波数に固定する効果があまり得られないばかりか、有害である場合があります。
理論上の HW 最大周波数である融合 GPU 最大周波数は、以下のように sysfs ハンドルを使用して取得できます。
for (( i=1; i<$num_cards; i++ ))dofor (( j=0; j<$num_tiles; j++ ))docat /sys/class/drm/card$i/gt/gt$j/rps_RP0_freq_mhz;
done
done

ユーザーは、rps_max_freq_mhz を使用して、動的に変更可能な現在の最大ソフトウェア周波数も取得できます。
for (( i=1; i<$num_cards; i++ ))dofor (( j=0; j<$num_tiles; j++ ))docat /sys/class/drm/card$i/gt/gt$j/rps_max_freq_mhz;
done
done

353

oneAPI GPU 最適化ガイド
サーバー用の Linux* カーネル・モード・ドライバー (i915) のデフォルトポリシーは、周波数要求範囲を次のように設定します。
rps_min_freq_mhz = rps_max_freq_mhz = rps_boost_freq_mhz = rps_RP0_freq_mhz

そのため、多くのワークロードは、このポリシーですぐに最高のパフォーマンスを得られる可能性があります。
ユーザーは、DRM Linux* カーネルドライバーで提供される以下の sysfs ハンドルを使用して周波数を設定できます。
for (( i=1; i<$num_cards; i++ ))dofor (( j=0; j<$num_tiles; j++ ))doecho $desired_freq > /sys/class/drm/card$i/gt/gt$j/rps_min_freq_mhz;
echo $desired_freq > /sys/class/drm/card$i/gt/gt$j/rps_max_freq_mhz;
echo $desired_freq > /sys/class/drm/card$i/gt/gt$j/rps_boost_freq_mhz;
done
done

周波数固定の注意事項:•••

ファームウェアは、許可された周波数を最終的に決定します。
サーマル/電力バジェットによってスロットリングが発生する場合があります。
周波数が固定されると動的スケーリングは行われません。サーバー・プラットフォームでは、i915 ポリシーは周波数をブート時に rps_max_freq_mhz に固定します。

5-19-4インテル® Xe リンクのオン/オフの切り替え

インテル® Xe リンクは、高速接続ファブリック・ハードウェアであり、スケールアップとスケールアウト操作に必要な高速データ転送機能を提供します。この機能は、クラスター規模で展開された HPC アプリケーションにおいて、GPU 間およびノード間のデータ転送に使用されます。この機能を使用しないアプリケーションでは、ハードウェア・リソースの電源をオフにして、GPU 計算エンジンを低周波数で動作させることが有益な場合もあります。
次のコマンドラインは、インテル® Xe リンク電源をオフにする方法を説明しています。
modprobe -r iaf;
for i in {0..$num_cards}; docat /sys/class/drm/card$i/iaf_power_enable;
done;
for i in {0..$num_cards}; doecho 0 > /sys/class/drm/card$i/iaf_power_enable;
done;
for i in {0..$numcards}; docat /sys/class/drm/card$i/iaf_power_enable;
done$num_cards は、/sys/class/drm/ ディレクトリーの一覧を表示し、card* サブ・ディレクトリーがいくつ存在す
るか確認できます。

354

oneAPI GPU 最適化ガイド

6 GPU 上でのメディア・グラフィックス計算
ここまでは、GPGPU の最適化手法とツールに焦点を当ててきました。メディア・グラフィックス・コンピューティングでは、ハードウェア、特にメディアエンジンなどの専用ユニットの利用率を最大化するため、新しい最適化手法とツールセットも重要です。


メディア・パイプラインの最適化
インテル® グラフィックス・パフォーマンス・アナライザーによるパフォーマンス解析

6-1 メディア・パイプラインの最適化
メディア処理は、入力と出力が明確に定義された比較的大きなアルゴリズムであるため、ハードウェア・アクセラレー
ションの理想的な候補であると言えます。ビデオ処理のハードウェア機能は、業界標準のフレームワークである
oneVPL や、Linux* 向けのビデオ・アクセラレーション API (VA-API) や Windows* 向けの Microsoft DirectX* などの低レベル/オペレーティング・システム固有のアプローチでアクセスできます。どの方法を選択するかは、多くの要因に影響されます。しかし、マルチストリームによる並列化やデータの局所性の最大化といった基本原則は、すべてに当てはまります。
ビデオ処理と GPGPU ワークの主な違いは、すべてのアクセラレーター API オプションに当てはまります。典型的な
GPGPU 最適化の多くは、大きなグリッドのワークを複数の処理ユニットに分割することに最適化の重点を置いています。ハードウェア・アクセラレーションによるメディア処理は、シリコン上に実装されています。これらはフレーム単位で動作し、通常、フレームストリームによってワークが分割されます。
メディア最適化のステップは、他のセクションで説明する GPGPU のワークロードと異なります。それらは GPGPU のワークの前後に追加することができます。メディアステップは、GPGPU ステップに入力データを供給、または GPGPU
ステップから出力データを取得します。例えば、次のようになります。

•
•
•
•
•

メディア・エンジン・ハードウェア
ハードウェア・アクセラレーション向けのメディア API オプション
メディア・パイプラインの並列処理
メディア・パイプラインの相互操作とメモリー共有
SYCL* によるぼかし (Blur) の例

355

oneAPI GPU 最適化ガイド
職場で会議をストリーミングしたり、自宅で動画を視聴したり、今日の世界では、ビデオ・ストリーミングが普及しており、高い品質が期待されます。新しいメディア・エンジン・ハードウェアを利用することで、ストリーミングを高速化した
り、高品質でストリーミングしたり、低電力でストリーミングすることができます。このハードウェア・ソリューションは、
ビデオデータを扱うパイプラインでは、エンドツーエンドのパフォーマンスを考慮する上で重要なものです。

6-1-1

メディア・エンジン・ハードウェア「インテル® Xe GPU アーキテクチャー」の章で説明したように、Xe インテル® データセンター GPU フレックス・シリーズ
Xe インテル® データセンター GPU フレックス・シリーズといくつかのインテル® GPU は、完全にアクセラレートされたビ
デオデコード、エンコードおよび処理能力を備えたビデオエンジンを搭載しています。これは、インテル® クイック・シン
ク・ビデオと呼ばれることもあります。メディアエンジンは、計算エンジン (ベクトルまたは行列エンジン) から完全に独立して動作します。

アプリケーションはいくつかのコンポーネントを利用できます。
•••

マルチフォーマット・コーデック (MFX): ハードウェア・デコードとエンコード、一部の構成では、1) 動き推測 + ビッ
ト・パッキング、2) 完全固定機能/低電力、の 2 つのエンコードを含みます。
スケーラーとフォーマット変換 (SFC): リサイズ (主にダウン・スケーリング向け)、NV12 や BGRA などのカラー・
フォーマット変換。
ビデオ品質エンジン: ノイズ除去やインターレースなど、複数のフレーム処理操作を実行。

このハードウェアは、独自の命令キューとクロックを持っているため、低消費電力で動作するように構成すれば、完全な固定機能のワークを低消費電力で実行できます。GPU スライスの機能をほかのワークに利用することもできます。

356

oneAPI GPU 最適化ガイド

6-1-1-1

サポートされるコーデック

GPU ハードウェアの世代が新しくなるたびに、新しいコーデックの機能が追加されています。

注: この表では、2 つのエンコードを表しています。
••

E = 低消費電力 VDEnc によるハードウェア・エンコード
Es = によるハードウェア・エンコード (PAK) + シェーダー (メディアカーネル + VME)

インテル® Arc™ A シリーズおよびインテル® サーバー GPU (Arctic Sound-M✝) に AV1 エンコードが追加されました。VP9 の後継となるこの最先端の技術は、タイル、セグメント化、フィルム・グレイン・フィルター処理、その他の新機能によるエンコード制御を追加します。これらの機能により、特定のビットレートでエンコード品質を向上させたり、
ビットレートを下げて品質を向上させることができます。

357

oneAPI GPU 最適化ガイド

6-1-2

ハードウェア・アクセラレーション向けのメディア API オプション

インテル® アーキテクチャー (CPU、GPU) でビデオ処理を高速化するには、いくつかの方法があります。最も効果的なオプションを選択するには、選択するツールと目的が一致していることを確認します。

このように、ツールには高レベルなものと低レベルなものがあります。libva* や DirectX* のような OS 固有のツールで提供される極めて低レベルの制御は必要ですか? また、このような低レベルのアプリケーションを開発する十分な
時間的余裕はありますか? または、Linux* から Windows* に容易にコードを移植でき、より高レベルのツールを利用してコーディングの時間を節約するほうが重要ですか?
アプローチの選択肢を要件に適合させるには、次の表を参考にしてください。
インテル® ビデオ・プロセシ
ング・ライブラリー

メディア・フレームワーク(FFmpeg* & GStreamer)

機能

フレーム処理演算を限定した初歩的なビデオストリーム
処理

ハードウェア機能の制御
レベル
移植性

中

完全なスタック (ネットワー
ク・プロトコル、コンテナのサポート、オーディオの
サポート)
低

高

高

6-1-3

低レベル/OS 固有のソリューション
(libva & DirectX)
OS グラフィックスのスタッ
クを直接操作高低

メディア・パイプラインの並列処理

GPGPU では、並列処理は ND Range の分割方法と関連するエッジ条件などに注目します。複数のアクセラレーターが分割された空間で動作し、グリッド全体で同じアルゴリズムを実行することができます (SIMD)。これは、エンコードとデコードの例ではありません。

358

oneAPI GPU 最適化ガイド
エンコーダーやデコーダーの内部実装を詳しく調査して、各フレームを処理する際に並列化の可能性を見つけるのではなく、通常は、処理全体をブラックボックスとして扱います。コーデックのデコード実装は、あるボックスを別のボックスに置き換えるように、交換可能であることを意図しています。エンコードの置き換えでは、さらに広範囲のパラメーターの影響を考慮する必要があるため複雑になります。しかし、考え方は同じであり、ホットスポットや内部ループの最適化ではなく、全体の最適化をハードウェアに最適なものに置き換えます。
理論的には、フレーム内をスライス単位で演算を並列化することができます。

これは、あまり実用的ではありません。モーション検索はスライス境界を検知できないため、スライス数が増加すると全体の圧縮品質に影響を与えます。スライスには追加のヘッダーバイトも必要になります。
単一のストリームを非同期で処理することができますが、スケーラブルではありません。フレーム間の依存関係が並列処理を妨げていますが、依存関係をオフにすると特定のビットレートで品質が低下します。処理中にフレーム数を増やすとレイテンシーも増加します。

単一ストリームの最適化では、インテル® Deep Link Hyper Encode によって開発が簡素化される可能性があります。
インテル® Deep Link Hyper Encode は、統合/プロセッサー ・グラフィックスも利用可能なシステムで 1 つ以上のディスクリート GPU が利用可能な場合に、統合 GPU とディスクリート GPU 間の作業を自動的に調整することでパ
フォーマンスを向上できます。単一ストリームのパフォーマンスは、dGPU と iGPU の機能を併用することで向上でき
ます。

359

oneAPI GPU 最適化ガイド

品質を維持しながらレイテンシーを軽減して効率良くスケーリングする最良の方法は、複数のストリームを同時に処理することです (注: 非リアルタイム処理では、フレームがセグメント/GOP の境界を越えて存在しないため、単一のストリームでもセグメントとして並列に処理できます)。

このアプローチは、アクセラレーター間でスケールする理想的な並列性を実現します。ストリーム間に依存関係がな
いため、各アクセラレーターは調整なしに可能な限り高速に処理を行うことができます。ハイパーエンコードの場合、
iGPU と dGPU に個別のストリームをスケジュールするほうが高速です。
oneAPI の観点から見ると、これらのプロパティーは SYCL* との相互運用性を大幅に簡略化します。メディア操作は、
通常カーネル内部では実行されないため、API や開発レベルでの考慮事項は少なくなります。メディア操作は、カーネ
ルにデータを供給する (ソースとして動作)、またはソースが供給するデータのシンクとして動作します。パフォーマンスに関する懸念事項は、メディア操作とカーネル間のハンドオフが同期につながり、単一ストリーム内で非同期に処理する可能性が減少することです。そのため、複数のストリームを同時に処理することが、この問題に対する最善の解決策となります。

6-1-3-1

メディア操作の最適化

アルゴリズムはハードウェアで実装されるため、メディア開発における主な懸念事項はデータの局所性、同期、そしてハードウェアを常にビジーにするワークのパイプラインを提供することです。

360

oneAPI GPU 最適化ガイド
データの局所性: フレームを GPU 上に維持し、不要な CPU へのコピーを避けます。メディアエンジンは、GPU のメモ
リー階層に接続されているため、データはスライスとメディアエンジンのコンポーネント間でローカルに共有できます。GPGPU の観点からは、これらの操作はローカル GPU のデータで動作します。フレームは、このハードウェアとエ
グゼキューション・ユニット間で低レイテンシー/コピーなしで共有されます。これは、ディスクリート GPU ではさらに重要であり、PCI バスで生フレームを移動するコストを軽減できます。
同期： 複数のハードウェア・ユニットは独立して機能するため非同期に動作することができます。最高のパフォーマンスを達成するには、アプリケーションは可能な限り頻繁に CPU との同期を強制する必要があります。アクセラレー
ターが中断することなく処理を続行できるようにアルゴリズムを設計します。
ハードウェアをビジーに保つ: 命令キャッシュがフルに維持されていないと、エンジンのクロックは低下します。フル
クロックに復帰するには数ミリ秒を要します。

6-1-4

メディア・パイプラインの相互操作とメモリー共有

メディアエンジンの機能は、次のような低レベルの OS 固有のインターフェイスで使用されます。
••

Linux* 向けの VA-API (ビデオ・アクセラレーション API)
Windows* 向けの Microsoft* DirectX* ビデオ・アクセラレーション

上記のような低レベルのインターフェイス上に構築される各種高レベル・メディア・フレームワークも同様です。
•••

インテル® ビデオ・プロセシング・ライブラリー (インテル® oneVPL)
FFmpeg* および libav*
GStreamer

それぞれのメディア・フレームワークは、デバイスとコンテキストの作成、メモリー割り当て、およびタスク投入の独自
インターフェイスを定義しています。また、大部分のフレームワークでは、メモリー・オブジェクトをほかのメモリーハ
ンドルに変換するエクスポート/インポート・インターフェイスも用意されています。
•••

高レベル・メディア・フレームワーク (FFmpeg*、GStreamer) は、低レベル・メディア・ハンドル (VA-API、DirectX* サーフェス) 間との変換をサポートしています。
低レベル・メディア・インターフェイス (VA-API、DirectX*) は、Linux* のバッファーや Windows* の NT ハンドルのような OS 固有の汎用 GPU メモリーハンドルへの変換をサポートしています。
DMA バッファー/NT ハンドルと USM デバイスポインター間のレベルゼロサポート変換。

これらのインターフェイスを組み合わせると、メディア・フレームワークを介して送信されたメディア操作と、SYCL*
キューに送信された SYCL* + 計算カーネル間で、ゼロコピーのメモリー共有が可能になります。これには、メディア・フレームワークと同一 GPU デバイス上に作成された SYCL* キューと SYCL* デバイスが、レベルゼロのバックエンド(OpenCL* バックエンドではなく) を使用すると仮定します。
メモリーハンドルの変換は、複数のステージ (FFmpeg*/GStreamer、VA-API/DirectX*、DNA/NT、レベルゼロ、
SYCL*) で行われますが、変換されたすべてのメモリーハンドルは同じ物理メモリーブロックを参照します。そのため、あるメモリーハンドルにデータを書き込むと、ほかのすべてのメモリーハンドルでそのデータを利用できます (書き込
みと読み取りの同期が適切に行われていることが前提)。
以下に、メディア・フレームワークと SYCL* 間でゼロコピーバッファーを共有する際に使用されるインターフェイスを
示します。

361

oneAPI GPU 最適化ガイド
1.

(Linux*) VA-API から DMA-BUF (英語)2.

(Windows*) DirectX から NT ハンドル (英語)3.

DMA-BUF または NT ハンドルからレベルゼロ (英語)DMA-BUF または NT ハンドルからレベルゼロが作成するメモリーポインター (上記 3) は、メディアメモリーの割り
当てとメディア操作で使用される同一 GPU デバイス上で動作する SYCL* カーネルからのみアクセス可能な USM デ
バイスポインターです。この USM ポインターは、ホストからはアクセスできず、CPU やほかの XPU デバイス上で動作する SYCL* カーネルからもアクセスできません。
次の項の例は、上記の 1 と 3 のインターフェイス、および合成ビデオデータ (移動する矩形) を使用して、VA-API とSYCL* 間でゼロコピーバッファーの共有を実証しています。GPU メディアエンジンの FFmpeg*/GStream ビデオデ
コード/エンコードと GPU 計算エンジン上の SYCL* カーネルを使用したさらに高度な例については、インテル® DL
Streamer メモリー相互運用 API (プレビュー) (英語) とインテル® DL Streamer のサンプル (英語) を参照してください。

6-1-4-1

VA-API と SYCL* メモリー共有の例

この節では次の例を紹介します。
1.

NUM_FRAMES フレームに対して、共有 VA-API サーフェスと USM デバイスポインターを割り当てます。

2.

フレーム上で移動する矩形を描画する VA-API 呼び出しを送信します。

3.

ステップ 2 で VA-API が作成した矩形の内側にサブ矩形を描画するため SYCL* カーネルを送信します。

4.

すべてのフレームを同期し、RGB データをファイルに書き込みます。

この例で生成される出力フレームは次のようになります。

この例は、Linux* 向けに作成されており、oneAPI パッケージのほかに以下に示す追加のパッケージをインストールする必要があります (Ubuntsu* で apt パッケージ・マネージャーを使用してインストールする例)。
$ sudo apt install intel-level-zero-gpu level-zero-dev$ sudo apt install intel-media-va-driver-non-free libva-dev libva-drm2362

oneAPI GPU 最適化ガイド
レベルゼロと VA-API ライブラリーの連携が必要です。
$ icpx -fsycl memory-sharing-with-media.cpp -lze_loader -lva -lva-drm

実行例では、いくつかのメディアプレーヤー (ex、ffplay など) で直接再生したリ、次のように FFmpeg* コマンドを使用して圧縮ビデオ・フォーマットに符号変換できるファイル (output.bgra) を生成しています。
$ FFmpeg* -f rawvideo -pix_fmt bgra -s 320x240 -i output.bgra output.mp4生成されたファイル (output.mp4) は、任意のメディアプレーヤーで再生できます。
$ ffplay output.mp4
1. // SYCL*
2. #include <CL/sycl.hpp>
3.
4. // SYCL* oneAPI 拡張
5. #include <sycl/ext/oneapi/backend/level_zero.hpp>
6.
7. // レベルゼロ
8. #include <level_zero/ze_api.h>9.
10. // VA-API
11. #include <va/va_drm.h>
12. #include <va/va_drmcommon.h>13.
14. #include <cstdio>
15. #include <fcntl.h>
16. #include <unistd.h>
17. #include <vector>
18.
19. #define OUTPUT_FILE "output.bgra"
20.
21. #define VAAPI_DEVICE "/dev/dri/renderD128"
22.
23. #define FRAME_WIDTH 320
24. #define FRAME_HEIGHT 240
25.
26. #define RECT_WIDTH 160
27. #define RECT_HEIGHT 160
28. #define RECT_Y (FRAME_HEIGHT - RECT_HEIGHT) / 2
29.
30. #define NUM_FRAMES (FRAME_WIDTH - RECT_WIDTH)
31.
32. #define VA_FORMAT VA_FOURCC_BGRA
33. #define RED 0xffff0000
34. #define GREEN 0xff00ff00
35. #define BLUE 0xff0000ff
36.
37. #define CHECK_STS(_FUNC)
38.
{
39.
auto _sts = _FUNC;
40.
if (_sts != 0) {41.
printf("Error %d calling " #_FUNC, (int)_sts);
42.
return -1;
43.
}
44.
}
45.
46. VASurfaceID alloc_va_surface(VADisplay va_display, int width, int height) {47.
VASurfaceID va_surface;48.
VASurfaceAttrib surface_attrib{};
49.
surface_attrib.type = VASurfaceAttribPixelFormat;
50.
surface_attrib.flags = VA_SURFACE_ATTRIB_SETTABLE;
51.
surface_attrib.value.type = VAGenericValueTypeInteger;363

oneAPI GPU 最適化ガイド
52.
surface_attrib.value.value.i = VA_FORMAT;
53.
vaCreateSurfaces(va_display, VA_RT_FORMAT_RGB32, width, height, &va_surface,54.
1, &surface_attrib, 1);55.
return va_surface;
56. }
57.
58. int main() {59.
// GPU デバイスとレベルゼロ・バックエンドで SYCL* キューを作成し、
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.

// レベルゼロ・コンテキストとデバイスを照会
sycl::queue sycl_queue{sycl::ext::oneapi::filter_selector("level_zero")}; // { sycl::gpu_selector_v }auto ext_level_zero = sycl::backend::ext_oneapi_level_zero;
auto ze_context = sycl::get_native<ext_level_zero>(sycl_queue.get_context());
auto ze_device = sycl::get_native<ext_level_zero>(sycl_queue.get_device());

83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.

// (VA-API とレベルゼロ間で共有されるゼロコピーバッファー)
void *device_ptr[NUM_FRAMES];
size_t stride;for (int i = 0; i < NUM_FRAMES; i++) {// VASurface から DMA-FD をエクスポート
VADRMPRIMESurfaceDescriptor prime_desc{};
CHECK_STS(vaExportSurfaceHandle(va_display, surfaces[i],VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
VA_EXPORT_SURFACE_READ_WRITE, &prime_desc));
auto dma_fd = prime_desc.objects->fd;
auto dma_size = prime_desc.objects->size;
stride = prime_desc.layers[0].pitch[0] / sizeof(uint32_t);

364// VA-API コンテキストの作成 (VADisplay)
VADisplay va_display = vaGetDisplayDRM(open(VAAPI_DEVICE, O_RDWR));if (!va_display) {printf("Error creating VADisplay on device %s\n", VAAPI_DEVICE);
return -1;}
int major = 0, minor = 0;
CHECK_STS(vaInitialize(va_display, &major, &minor));// VA-API サーフェスの作成
VASurfaceID surfaces[NUM_FRAMES];
for (int i = 0; i < NUM_FRAMES; i++) {surfaces[i] = alloc_va_surface(va_display, FRAME_WIDTH, FRAME_HEIGHT);}// 各 VA-API サーフェスを USM デバイスポインターに変換// DMA-FD をレベルゼロ・デバイス・ポインターにインポート
ze_external_memory_import_fd_t import_fd = {ZE_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMPORT_FD,
nullptr, // pNext
ZE_EXTERNAL_MEMORY_TYPE_FLAG_DMA_BUF, dma_fd};
ze_device_mem_alloc_desc_t alloc_desc = {};alloc_desc.stype = ZE_STRUCTURE_TYPE_DEVICE_MEM_ALLOC_DESC;
alloc_desc.pNext = &import_fd;
CHECK_STS(zeMemAllocDevice(ze_context, &alloc_desc, dma_size, 1, ze_device,&device_ptr[i]));// DMA-FD をクローズ
close(dma_fd);}// サイズ 1 x 1 の VA-API サーフェスを作成し、GREEN のピクセルを書き込み
VASurfaceID surface1x1 = alloc_va_surface(va_display, 1, 1);
VAImage va_image;
void *data = nullptr;
CHECK_STS(vaDeriveImage(va_display, surface1x1, &va_image));oneAPI GPU 最適化ガイド
116.
117.
118.
119.
120.
121.

CHECK_STS(vaMapBuffer(va_display, va_image.buf, &data));*(uint32_t *)data = GREEN;CHECK_STS(vaUnmapBuffer(va_display, va_image.buf));
CHECK_STS(vaDestroyImage(va_display, va_image.image_id));// 背景を BLUE に塗りつぶし、1 x 1 サーフェスを移動する

122. // GREEN の矩形にスケールアップする VA-API 呼び出し
123. VAConfigID va_config_id;124. VAContextID va_context_id;
125. CHECK_STS(vaCreateConfig(va_display, VAProfileNone, VAEntrypointVideoProc,
126.
nullptr, 0, &va_config_id));127. CHECK_STS(vaCreateContext(va_display, va_config_id, 0, 0, VA_PROGRESSIVE,
128.
nullptr, 0, &va_context_id));129. for (int i = 0; i < NUM_FRAMES; i++) {130.
VAProcPipelineParameterBuffer param{};131.
param.output_background_color = BLUE;132.
param.surface = surface1x1;
133.
VARectangle output_region = {int16_t(i), RECT_Y, RECT_WIDTH, RECT_HEIGHT};
134.
param.output_region = &output_region;
135.
VABufferID param_buf;
136.
CHECK_STS(vaCreateBuffer(va_display, va_context_id,
137.
VAProcPipelineParameterBufferType, sizeof(param),138.
1, &param, &param_buf));139.
CHECK_STS(vaBeginPicture(va_display, va_context_id, surfaces[i]));140.
CHECK_STS(vaRenderPicture(va_display, va_context_id, &param_buf, 1));141.
CHECK_STS(vaEndPicture(va_display, va_context_id));142.
CHECK_STS(vaDestroyBuffer(va_display, param_buf));143. }
144.
145. #if 0
146.
// Linux* では、i915 KMD ドライバーがインテルのメディアおよび計算ドライバーから
147.
// 送信されたリード/ライトを同期するため、同期はオプションです148.
for (int i = 0; i < NUM_FRAMES; i++) {149.
CHECK_STS(vaSyncSurface(va_display, surfaces[i]));150.
}
151. #endif
152.
153. // SYCL* カーネルを送信して、GREEN の矩形に RED のサブの矩形を書き込み
154. std::vector<sycl::event> sycl_events(NUM_FRAMES);
155. for (int i = 0; i < NUM_FRAMES; i++) {156.
uint32_t *ptr = (uint32_t *)device_ptr[i] +157.
(RECT_Y + RECT_HEIGHT / 4) * stride + (i + RECT_WIDTH / 4);158.
sycl_events[i] = sycl_queue.parallel_for(159.
sycl::range<2>(RECT_HEIGHT / 2, RECT_WIDTH / 2), [=](sycl::id<2> idx) {160.
auto y = idx.get(0);161.
auto x = idx.get(1);162.
ptr[y * stride + x] = RED;163.
});
164. }
165.
166. // すべての SYCL* カーネルを同期
167. sycl::event::wait(sycl_events);
168.
169. // VA-API サーフェスをシステムメモリーにマップしてファイルに書き込み
170. FILE *file = fopen(OUTPUT_FILE, "wb");
171. if (!file) {172.
printf("Error creating file %s\n", OUTPUT_FILE);
173.
return -1;
174. }
175. for (int i = 0; i < NUM_FRAMES; i++) {176.
CHECK_STS(vaDeriveImage(va_display, surfaces[i], &va_image));
177.
CHECK_STS(vaMapBuffer(va_display, va_image.buf, &data));178.
fwrite(data, 1, FRAME_HEIGHT * FRAME_WIDTH * 4, file);
179.
CHECK_STS(vaUnmapBuffer(va_display, va_image.buf));180.
CHECK_STS(vaDestroyImage(va_display, va_image.image_id));181. }

365

oneAPI GPU 最適化ガイド
182. fclose(file);
183. printf("Created file %s\n", OUTPUT_FILE);
184.
185. // デバイスポインターと VA-API サーフェスを解放
186. for (int i = 0; i < NUM_FRAMES; i++)187.
zeMemFree(ze_context, device_ptr[i]);188. vaDestroySurfaces(va_display, surfaces, NUM_FRAMES);
189.
190. return 0;
191. }

6-1-5

SYCL* によるぼかし (Blur) の例

これから概念を組み合わせた oneVPL の例 (現在は単一ストリームのみ) については、sycl-blur を参照してください。このサンプルでは、次の手順でフレームを入力、操作、出力するビデオ API と oneVPL 間とのメモリー相互運用を
示します。
•
•
•
•

SYCL* を設定
oneVPL のセッションを設定
oneVPL VPP を初期化
フレームで以下を繰り返し‐ フレームをファイルから読み取り‐ GPU 上で VPP リサイズ/カラー空間変換を実行‐ GPU サーフェスへのアクセス権を取得して USM へ変換‐ GPU で SYCL* カーネル (blur) を実行‐ フレームをファイルへ出力

このサンプルは、https://github.com/oneapi-src/oneVPL/tree/master/examples/interop/dpcpp-blur (英語)から入手できます。
この例から oneVPL と SYCL* 間の相互作用は、フレームレベルであることが分かります。oneVPL がフレームを供給
して SYCL* カーネルがそれを処理します。L0 (Linux*) でゼロコピー機能が有効な OS 環境では、libva* のフレーム
データは USM として SYCL* に供給されます。libva* の生フレームを新しい USM サーフェスにコピーする代わりに、アプリケーションは GPU 上のフレームを libva* サーフェスとして処理し、USM の場合と同じようにメモリーで操作を開始できます。
この例はさらに簡単にできますが、多くの設計を簡素化し、ゼロコピーの利点を十分に紹介できないのが残念です。
•••

生フレームをストレージから読み取り、ストレージに書き込む。これは全体のフレームレートに影響します。
VPP データは、システムメモリーとして読み取られ、ビデオメモリーに変換されます。
パイプラインは各フレーム間で同期されます。

ゼロコピーは、ハイパフォーマンスなアプリケーションの核となる概念です。

366

oneAPI GPU 最適化ガイド

6-2 インテル® グラフィックス・パフォーマンス・アナライザーによるパフォーマンス解析
6-2-1

概要

インテル® グラフィックス・パフォーマンス・アナライザー (インテル® GPA)
インテル® GPA は、シングル/マルチ CPU プラットフォーム、およびシングル/マルチ GPU プラットフォームで実行されるアプリケーションを解析するパフォーマンス解析ツールです。GUI とコマンドスクリプトの両方でデータを詳しく解析できます。
インテル® GPA は、4 つの GUI ツールとコマンドライン・ツールで構成されます。
•

GUI ツール‐ Graphics Monitor - インテル® GPA ツールのハブであり、トレースおよびフレーム・キャプチャーを開始できます。
‐ System Analyzer - CPU と GPU のアクティビティーをランタイムに解析します。
‐ Graphics Trace Analyzer - 数秒間のデータ・キャプチャーで、CPU/GPU 間の相互作用を詳しく解析できます。
‐ Graphics Frame Analyzer - 単独または複数のフレームで GPU のアクティビティーを詳しく解析できます。

•

コマンドライン・インターフェイス・ツール‐ インテル® GPA Framework - スクリプト化が可能なコマンドライン・インターフェイスにより、単独または複数のフレームから広範囲なデータを取得して解析できます。

この章では、GPU の動作を詳しく理解するため、Graphics Frame Analyzer の機能に注目します。
(注: インテル® GPA Framework は、Graphics Frame Analyzer のバックエンドであり、同じデータを取得することができます。また、インテル® GPA Framework を使用すると、プロファイル作業を自動化できます。)
Graphics Frame Analyzer の機能
Graphics Frame Analyzer が提供する便利な機能の一部を紹介します。
•

一連のフレームのうち、どのフレームに最も時間がかかるか即座に確認できます。

•

高度なプロファイル・モードを使用すると、パイプラインの状態と GPU メトリックからアプリケーションで最もホットなボトルネックを自動的に計算するためあるフレームに対して最適化を行うと、アプリケーションのほかの部分も最適化される可能性があります。

•

ジオメトリー - ワイヤーフレームとソリッド。
‐ 画面外に描画したか数秒で確認できます。
‐ ジオメトリーをあらゆる角度から動的に観察できます。

•

テクスチャー‐ 描画呼び出しごとにテクスチャーを可視化できます。
‐ 持続時間が長い描画呼び出しが、フレーム内の重要ではない部分を描画していないか確認します。
367

oneAPI GPU 最適化ガイド•

シェーダー‐ 最も時間を要するシェーダー・コマンドラインを特定します。
‐ 各 DXR (DirectX* レイトレーシング) シェーダーの呼び出し回数や、シェーダー EU の占有率などを観察できます。

•

レンダリング状態の確認 - ボタンをクリックするだけで、テクスチャーやピクセル、イベントを無効にしてボトル
ネックの原因を特定し、メトリックやその他のデータの変化をすぐに確認できます。

サポートされる API•••

Direct3D* 11
Direct3D* 12 (DirectX* 12 Ultimate を含む)
Vulkan*6-2-2

実行ユニットのストール、アクティブ、スループット

Graphics Frame Analyzer は、フレームの持続期間、API 呼び出し、リソースデータ、イベントデータなどを素早く確認できる、初心者から上級者まで利用可能な強力なツールです。各データ要素が意味することの理解度を深めること
で、パフォーマンスの問題の根本的な原因を特定しやすくなります。
実行ストール、実行アクティブ、実行スループット
これら 3 つのデータ要素の相互関係を解釈する方法を知ることは、GPU に関連するアプリケーションの相互作用をさらに深く理解することにつながります。
EU、XVE (Xe ベクトルエンジン) およびインテル® XMX
このドキュメントの「インテル® Xe GPU アーキテクチャー」の項で説明したように、Xe LP および前世代のインテル®
GPU では、EU (エグゼキューション・ユニット) が GPU の計算ユニットとなります。Xe HPG と Xe HPC では、計算ユニットに Xe コアを導入しています。後者のプラットフォームでは、各 Xe コアは ALU (算術論理演算ユニット) — 8 つのベクトルエンジン (XVE) と 8 つの行列拡張 (XMX) ― で構成されます。
Graphics Frame Analyzer が Xe LP またはそれ以前のアーキテクチャーで実行される場合、EU アクティブ、EU ス
トール、および EU スループットのデータラベルが表示されます。新しいアーキテクチャーでは、XVE アクティブ、XVE
ストール、および XVE スループットのデータラベルが表示されます。ここでは、リファレンス・アーキテクチャーとして
Xe LP を使用するため、EU を表記します。しかし、EU であろうが XVE であろうが、ストール/アクティブ/スループットの関係は同じようにパフォーマンスに影響することを忘れないでください。
3 つのデータ要素を理解する
始めに、96 個の EU を持つ Xe LP のユニット全体から、1 つの EU にドリルダウンして見ていきます。この GPU の各
EU の汎用レジスターファイル (GRF) は、7 つのスレッドを保持します。次の図は、GPU 全体から単一の EU へのドリ
ルダウンを示しています。

368

oneAPI GPU 最適化ガイド
単一の EU にドリルダウン

EU を詳しく見ていきます。次の図に示される要素は、主に GRF の 7 つのスレッドスロットに焦点を当て、SEND ユ
ニットおよび 2 つの ALU とスレッドの相互作用の重要性を示しています。
GRF、ALU、スレッド制御 (Thread Control)、分岐 (Branch)、および送信 (SEND) ユニット

スレッド化のシナリオについて考えてみます。次の図に GRF の内容を示します。この EU の GRF には、この時点で 1
つのスレッドのみがロードされていることが分かります。つまり、単一スレッドが一定時間実行されていることを示しています。これは、命令を実行するため ALU が 1 つ、または 2 つ呼び出されることを意味します。ある時点で、スレッドはデータをリードまたはライトする必要があり、これにより SEND ユニットがアクティブになります。スレッドがリード/ライトの完了を待機する間、EU はストールします。スレッドはロードされていますが、何も計算を行いません。

369

oneAPI GPU 最適化ガイド
GRF には単一のスレッドがある

このシナリオを拡張した次の図では、EU に 2 つ目のスレッドがあることが分かります。EU の GRF に 2 つ目のスレッドがロードされている場合、最初のスレッドが SEND ユニットを呼び出した時点で実行を停止する代わりに、EU は 2
つ目のスレッドの命令を実行します。2 つ目のスレッドが SEND ユニットを使用するコマンドを呼び出すと、最初のスレッドが実行可能になるまで EU はストールします。この状況で EU に 3 つ目のスレッドがある、または最初のスレッドが SEND 処理を終え実行可能である場合、すべての待機時間が隠匿されスレッドには待機が生じる可能性があり
ますが、EU はストールすることはないでしょう。
EU がデータのリード/ライトを待機する間 ALU は何も行わない (命令を実行しない)

用語
以下の定義において、計算された結果データは、
•
•

370

すべての EU の平均値を示します。
フレーム全体のデータと、フレームの選択された部分のデータの両方で構成されます。選択されるのは、1 つの呼び出しでも、呼び出しのセットでも、フレームのセットでもかまいません。

oneAPI GPU 最適化ガイド
アイドル
EU アイドルは、スレッドがロードされていない時間のパーセンテージです。
アクティブ
EU アクティブは、ALU0 または ALU1 が何らかの命令を実行していた時間のパーセンテージです。この値が低い原
因は、ストールが多いか EU がアイドル状態であることです。
ストール
EU ストールは、1 つ以上のスレッドがロードされているにもかかわらず、リードまたはライトを待機しているため、どのスレッドも実行できない時間のパーセンテージを示します。
スレッド占有率
EU スレッドの占有率は、GRF スロットを占有した (スレッド負荷)パーセンテージです。これは可能な限り高い値にする必要があります。EU スレッドの占有率が低い場合、バーテックス・フェッチやスレッドのディスパッチなど先行する
H/W ブロックのボトルネック、または計算シェーダーである場合は適切でない SIMD 幅や共有メモリーの利用を示します。
96 個の EU がそれぞれ 1 つのスレッドを実行すると、7 スロット/EU のうち 1 スロットが占有され、スレッドの占有率
は 1/7 (14%) になります。
2 つの EU にスレッドがロードされておらず (アイドル状態) で、ほかの 94 の EU に 6 つのスレッドがロードされていると仮定すると、占有率 = (0 + 0 + 6 * 94) / 672 = 84% になります。
Graphics Frame Analyzer に表示されるスレッドの占有率 (Thread Occupancy) 値は、選択した時間範囲におけるすべての EU の平均占有率を示しています。ほかのハードウェアでは、EU や XVE の数が異なるかもしれませんが、計算はすべてのエグゼキューション・ユニットで行われます。例えば、次の図の左に表示されるフレームには、フレーム全
体の時間 (6ms) でスレッド占有率は変化していますが、96 個すべての EU の時間平均は 77% となっています。また、フレーム期間中の一部のスレッド占有率も確認できます。図の右側では、最も時間がかかる 3 つの描画呼び出しを選択して、アプリケーションのこれらのビットを実行するのに要した 1.9 ミリ秒間のスレッド占有率が 85.3% であることが確認できます。

371

oneAPI GPU 最適化ガイド

6-2-3Graphics Frame Analyzer

このデータをインテル® GPA の Graphics Frame Analyzer で確認します。使い方を説明する 8 本のビデオ (英語)と記事が用意されています。
フレームを開いた直後の Graphics Frame Analyzer では、次の図のような表示を確認できます。フレームを開くとフレーム全体のデータのパーセンテージ値が表示されます。これは、アクティブ、ストール、スループットなどのデータがフレーム時間で 96 EU を平均したパーセンテージを意味します。
データ値はすべてのフレーム時間における全 EU の平均値

372

oneAPI GPU 最適化ガイド
その後、単独の描画呼び出しまたは呼び出しセットを選択すると、選択したフレームに応じてデータが再計算されます。
選択後 (この場合 91、94、95 の呼び出し)、これらの呼び出しデータのみを表示するよう再計算

さらに、ストリームをキャプチャーするとマルチフレーム表示で開きます。また、単一のフレームや複数のフレームを選択できます。複数のフレームを選択した場合、再計算されるデータは選択されたすべてのフレームデータの合計になります。
効率良くプロファイルを行うには、GPU がどのように動作してメトリックが何を意味しているか理解することが重要ですが、問題のタイプを知るためフレーム内の描画呼び出しを手動で解析する必要はありません。そのような状況での解析を支援するため、インテル® GPA は自動ホットスポット解析 (高度なプロファイル・モード) を提供します。
ホットスポット解析
EU アーキテクチャーをある程度理解できたら、その知識をどのようにプロファイルに活用できるか見ていきましょう。
高度なプロファイル・モードを有効にすると、Graphics Frame Analyzer はボトルネックのタイプとパイプラインの状態によってボトルネックを特定します。このように分類することで、ある問題を修正することで、その部分的な問題だけ
ではなく問題のカテゴリー全体が影響を受けることが多いという利点もあります。
Graphics Frame Analyzer ツールの左上にあるボタンをクリックして、ホットスポット解析を有効にします (以下の図を参照)。上部のバーチャートにボトルネックが表示され、左ペインの [API Log] がボトルネックのタイプを示すように変化します。ボトルネックをクリックすると、メトリックビューにボトルネックの詳細が表示されます (メトリックの説
明、ボトルネックを軽減するヒントなど)。

373

oneAPI GPU 最適化ガイド
ホットスポット: L2 キャッシュ
L2 キャッシュ・ホットスポットの特性
アプリケーションのスレッド占有率が高く 90% 近くある場合は問題ありません。しかし、スレッド占有率が高く、5～
10% 以上のストールがある場合は、L2 キャッシュがボトルネックになっている可能性があります。
Graphics Frame Analyzer でフレームを開き、右にある [Metrics Viewer] パネルをご覧ください。占有率は 90%以上ですが、EU にはまだストールが残っています。これは EU スレッドがメモリーからのデータを待機していることを意味します。

Shader Profiler
さらに詳しく解析するには、Shader Profiler を使用して命令ごとの実行レイテンシーを確認します。皆さんもご存じのように、レイテンシーとストールは必ずしも同じではありません。レイテンシーが大きい命令ほどストールする可能性が高くなります。EU ストールのボトルネックを調査する場合、Shader Profiler はストールの原因となる可能性が高い命令とその情報を提供します。
Shader Profiler を有効にする
次に示すように Shader Profiler にアクセスいます。[Resource] リストの任意のシェーダー (ここでは SH:17) をクリップすると、[Resource] パネルにシェーダーコードが表示されます。次の [Resource] ペインの上部にあるフレー
ムボタンをクリックすると、最も時間を要する行にタイミングが注釈されたシェーダーコードが表示され、実行回数と持続時間 (フレーム時間の消費パーセンテージ) を切り替えできます。

374

oneAPI GPU 最適化ガイド
アセンブリー・コードにシェーダーコードをマップ
L2 キャッシュがボトルネックである可能性がある場合、SEND ユニットからの送信コマンドを含むアセンブリー・コー
ドを調査する必要があります。シェーダーコードの上にある [Resource] パネルの ボタンをクリックすると、シェー
ダーコードからアセンブリー・コードへのマッピングが示されます。

L2 ボトルネックの原因を特定
L2 キャッシュがボトルネックとなる根本的な原因を見つけるには、アセンブリー・コードをスクロールして最も時間がかかる SEND 命令を探します。そして、それらに関連するシェーダーコード領域を特定します。
図の場合、ShadowMap からサンプリングして定数バッファーをリードする CalcUnshadowedAmountPCF2x2 関数
がボトルネックの原因となっています。
ホットスポット: シェーダー実行
シェーダー実行のボトルネックの特性
シェーダー実行のボトルネックには、スレッド占有率が極めて高く、ストール時間が短い特徴があります。これらは許容できます。しかし、アプリケーションの実行時間全体を短縮するのであれば、シェーダーコード自体を最適化する必要があります。

375

oneAPI GPU 最適化ガイド

シェーダー実行ボトルネックの原因を特定
シェーダー実行のボトルネックを見つけるには、シェーダーコードの演算によるホットスポットを解析する必要があります。これには、Shader Profiler を持続時間モード (Duration Mode) に切り替えて、コードをスクロールし、長い実行時間を要するシェーダーコード行を見つけます。CalcLightingColor 関数は、単純演算と超越演算の両方を含む計算を行います。図は、単一シェーダーに含まれるこの関数が、フレーム時間全体のおよそ 20% を消費していることを示しています。このボトルネックを解消するには、アルゴリズム自体を最適化する必要があります。

ホットスポット: スレッドのディスパッチ
スレッド・ディスパッチのボトルネックの特性
ホットスポット解析の最後の例には、スレッドのディスパッチがボトルネックになっている描画呼び出しのシーケンスがあります。このケースでは、ストール率がかなり高く (20%)、スレッド占有率が低く (66%) なっています。前述のように、低い占有率は EU にロードされているスレッド数が不足していることが原因である可能性があります。これには、シェーダーコードを修正してストール時間を改善するのではなく、EU 全体の占有率を高める必要があります。

376

oneAPI GPU 最適化ガイド

スレッド・ディスパッチのボトルネックの原因を特定
SIMD8 と SIMD16 のどちらが適切でしょう?
Shader Profiler を 開 い て 、 持 続 時 間 モ ー ド で は な く 、 命 令 が 何 回 実 行 さ れ た か カ ウ ン ト す る 実 行 カ ウ ン ト(Execution Count) モードに切り替えます。図では、ピクセルシェーダーが SIMD8 と SIMD16 の両方にコンパイルされていることに注目してください。Shader Profiler のレポートを見ると、SIMD8 の命令は 24,000 回実行されていますが、SIMD16 の命令実行は 16,000 回であり、1.5 倍の差があることが分かります。
SIMD16 のスレッドは SIMD8 のスレッドに比べ、一度のディスパッチで 2 倍の演算が可能であるため、多くの
SIMD16 スレッドを実行することが望ましいと考えることができます。なぜ SIMD8 スレッドがディスパッチされるのでしょうか? ピクセルシェーダーに 2 つの SIMD バ―ションが必要な理由を考えてみましょう。

377

oneAPI GPU 最適化ガイド
ジオメトリーの調査
これらの描画呼び出しのジオメトリーは、かなり粒度が細かくなっています。観測された異常性は、GPU がピクセル・
シェーディングを行う方法に起因します。シェーダー・コンパイラーは、SIMD8 と SIMD16 の 2 つのバージョンを生成します。これは、ピクセルシェーダーのディスパッチャーがラスタライズの結果に基づいて、実行する処理を選択できるようにするため必要なことです。ハードウェアは、ピクセルを 1 つずつシェードするわけではないことを理解する必要があります。シェーディングはグループ単位で行われます。1 つのピクセル・シェーダー・ハードウェア・スレッドは、
SIMD16 を使用するため一度に 16 ピクセルをシェーディングします。SIMD16 では、プリミティブのラスタライズが少ない、あるいは 1 つしかない場合でも、GPU は 16 ピクセルをシェーディングして不要なピクセルを廃棄します。そのため、ピクセルシェーダーのディスパッチャーは廃棄を少なくするため、小さなプリミティブに対して SIMD8 をスケジュールします。ここでは、このケースを想定しています。忠実度が高いジオメトリー (小さなプリミティブが多い) が多いため、膨大な数の SIMD8 呼び出しが発生しました。このようなパフォーマンスの問題を解決するには、ゲーム内
でジオメトリー LOD を使用する必要があることは、想像のとおりです。

まとめ
ボトルネックのタイプ
L2 キャッシュ
シェーダー実行
スレッドのディスパッチ

特性化
高占有率
高ストール
高占有率
低ストール
高ストール
低占有率

このように、シナリオが異なればアプローチも異なります。ときには、GPU をフル稼働させるため、CPU のワークをス
ピードアップさせたほうが良い場合もあります。一方、シェーダーコードを最適化するほうが適しているケースもあります。また、プリミティブのフォーマットや次元、レイアウトを変更するほうが良い場合もあります。それぞれのシナリ
オで、Graphics Frame Analyzer はリソースの解析を容易にし、開発者がアプリケーションのフレームレートを最適化する方法を決定するのを支援します。
インテル® GPA を使用して GPU のパフォーマンスを最適化する方法は、インテル® GPA の使用事例 (英語)、ディープ
ダイブとクイックヒント (英語) を参照してください。

378

oneAPI GPU 最適化ガイド

7 参考資料
詳細については、以下を参照してください。
•
•
•
•
•
•
•
•
•
•
•
•

インテル® oneAPI DPC++/C++ コンパイラー デベロッパー・ガイドおよびリファレンス (英語)インテル® oneAPI プログラミング・ガイド
インテル® Fortran コンパイラー・クラッシックおよびインテル® Fortran コンパイラー・デベロッパー・ガイドとリ
ファレンス (英語)インテル® oneAPI DPC++/C++ コンパイラーおよびインテル® Fortran コンパイラー向けの GPU へのOpenMP オフロード導入ガイド (英語)インテル® oneAPI DPC++/C++ コンパイラーにおける OpenMP* 機能と拡張のサポート
インテル® Fortran コンパイラーで実装される Fortran 言語と OpenMP* 機能
インテル® oneAPI マス・カーネル・ライブラリー・デベロッパー・リファレンス - C (英語)
OpenMP API 5.2 の仕様 (英語)
OpenMP API 5.1 の例 (英語)『Data Parallel C++』 James Reinders 共著 (英語)SYCL* 2020 仕様 リビジョン 6 日本語版
oneAPI ゼロレベル仕様 (英語)379

oneAPI GPU 最適化ガイド

8 法務上の注意書き
本資料は、(明示されているか否かにかかわらず、また禁反言によるとよらずにかかわらず) いかなる知的財産権のラ
イセンスも許諾するものではありません。
インテルは、明示されているか否かにかかわらず、いかなる保証もいたしません。ここにいう保証には、商品適格性、特定目的への適合性、および非侵害性の黙示の保証、ならびに履行の過程、取引の過程、または取引での使用から生
じるあらゆる保証を含みますが、これらに限定されるわけではありません。
本資料には、開発中の製品、サービスおよびプロセスについての情報が含まれています。ここに記載されているすべての情報は、予告なく変更されることがあります。インテルの最新の予測、スケジュール、製品仕様およびロードマップ
をご希望の方は、インテルの担当者までお問い合わせください。
本資料で説明されている製品には、エラッタと呼ばれる設計上の不具合が含まれている可能性があり、公表されている仕様とは異なる動作をする場合があります。現在確認済みのエラッタについては、インテルまでお問い合わせください。
性能に関するテストに使用されるソフトウェアとワークロードは、性能がインテル® マイクロプロセッサー用に最適化されていることがあります。
SYSmark* や MobileMark* などの性能テストは、特定のコンピューター・システム、コンポーネント、ソフトウェア、
操作、機能に基づいて行ったものです。結果はこれらの要因によって異なります。製品の購入を検討される場合は、他の製品と組み合わせた場合の本製品の性能など、ほかの情報や性能テストも参考にして、パフォーマンスを総合的に評価することをお勧めします。さらに詳しい情報をお知りになりたい場合は、http://www.intel.com/benchmarks/(英語) を参照してください。
性能の測定結果はシステム構成の日付時点のテストに基づいています。また、現在公開中のすべてのセキュリティー・
アップデートが適用されているとは限りません。詳細については、公開されている構成情報を参照してください。絶対的なセキュリティーを提供できる製品またはコンポーネントはありません。
実際の費用と結果は異なる場合があります。
インテルのテクノロジーを使用するには、対応したハードウェア、特定のソフトウェア、またはサービスの有効化が必要となる場合があります。
Intel、インテル、Intel ロゴ、その他のインテルの名称やロゴは、Intel Corporation またはその子会社の商標です。
* その他の社名、製品名などは、一般に各社の表示、商標または登録商標です。
©Intel Corporation.

380