目次
このタスクは、C 言語コースとインテルとのコラボレーションです。主な目的は、Intel が提供する並列行列乗算計算方法を学習して理解し、並列マージ ソート アルゴリズムと画像畳み込み並列加速をさらに独立して実装することです。この記事のコードの著作権は Intel Corporation に帰属します。転載の際は出典を明記してください。
1. 説明
oneAPI に基づいて行列乗算演算を実行する C++/SYCL プログラムを作成します。大きなサイズの行列の乗算演算と、異なるスレッド間のデータの依存関係を考慮する必要があります。通常、行列乗算を実装する場合、ブロック行列乗算と共有メモリを使用して計算効率を向上させることができます。
2. 分析
SYCL ベースのプログラミング モデルを使用して、GPU で行列乗算計算を実装します。手順は次のとおりです。
- メモリの割り当て: 入力行列と出力行列を保存するためにホスト側にメモリ空間を割り当て、対応する入力および出力データを保存するために GPU 側にメモリ空間を割り当てます。
- データ転送: 入力行列データをホスト側メモリから GPU 側メモリに転送します。
- カーネル関数呼び出し: SYCL では、行列乗算の計算は通常、GPU 上のカーネル関数を使用して並列計算を実現します。カーネル関数は、スレッド ブロックとスレッドを割り当てて、さまざまなデータ ブロックを処理します。
- 並列計算: カーネル関数では、各スレッドが出力行列の個別の要素を計算します。 GPU の並列計算機能を最大限に活用するために、通常は 2 次元のスレッド ブロックとスレッド グリッドを使用して行列の乗算計算を処理します。
- データ転送: 計算が完了すると、出力行列データはさらなる処理または分析のために GPU 側のメモリからホスト側のメモリに転送されます。行列の乗算を並列で計算する場合、スレッド ブロックとスレッドの階層を利用して計算を最適化できます。行列データを合理的に分割し、共有メモリを使用してグローバル メモリ アクセスの数を削減することで、計算効率を大幅に向上させることができます。さらに、GPU 上の複数のコンピューティング ユニットを活用し、行行列の乗算を実行してコンピューティング速度をさらに向上させることができます。
3. 概要と声明
1. この実験では、主に複数の方法を使用して並列行列乗算を実装し、最後にいくつかの方法を比較および分析します。
2. 声明: この実験はインテルの公式並列行列乗算コードのガイダンスに基づいており、これに基づいて若干の変更が加えられています。
4. 実装 1: SYCL に基づく基本的な並列行列乗算アルゴリズム
説明: SYCL 基本並列カーネルを使用して行列乗算を実装します。これは、最適化を行わずに SYCL を使用した最も単純な実装です。
4.1 加速デバイスの選択
後続のコンピューティング タスクを実行するデバイスを選択します。選択したデバイスは、行列乗算を実行するデバイスなど、後続の計算のターゲット デバイスとして使用されます。 oneAPI を使用すると、同じコードを異なるハードウェア アクセラレータで実行できるため、柔軟性とパフォーマンスが向上します。
import ipywidgets as widgets
device = widgets.RadioButtons(
options=['GPU Gen9', 'GPU Iris XE Max', 'CPU Xeon 6128', 'CPU Xeon 8153'],
value='CPU Xeon 6128',
description='Device:',
disabled=False
)
display(device)
4.2 行列乗算用の SYCL カーネル
このカーネルは基本的な行列乗算を実装しており、さまざまなデバイスで実行できます。
-
mm_kernel
関数は SYCL キューq
を受け入れ、3 つの行列matrix_a
、matrix_b
、< を参照します。 a i=4> であり、それぞれ行列のサイズを示す 2 つのパラメータ と があります。matrix_c
N
M
-
関数内では、まず行列のサイズ情報が出力されます。
-
は 3 つのバッファ (
buffer
) を作成しました。a
、b
、c
は、行列データを保存します。これらのバッファはデバイス上で評価されます。 -
q.submit
を使用してコマンド グループをデバイスに送信します。このコマンド グループでは、3 つのアクセサー (accessor
) が作成され、ホスト メモリからデバイス (A
および <) にデータをコピーするために使用されます。 a i=4>)、結果をデバイスからホストにコピーして戻します ()。B
C
-
h.parallel_for
は並列計算を開始し、2 次元のインデックス付けを通じて行列要素にアクセスします。各要素(i, j)
について、行列の 3 番目の次元を合計する内部ループを使用して行列の乗算の結果を計算します。 -
c.get_access<access::mode::read>()
計算結果を後で読み取れるように、ホスト側でカーネルの実行が完了するまで必ず待機してください。 -
イベントのプロファイリング情報を通じてカーネルの実行時間を計算して出力します (
e
)。
使用する場合は、SYCL キューを作成し、入力行列を mm_kernel
関数に渡して行列の乗算を実行する必要があります。
コードのコメントでは、質問 1 の分析と計画の手順 (メモリ割り当て、データ送信、カーネル関数の呼び出し、並列計算、データ送信) に従って説明します。
%%writefile lab/basic_matrix.cpp
//版权归属:Copyright © 2021 Intel Corporation
#include <CL/sycl.hpp>
using namespace sycl;
void mm_kernel(queue &q, std::vector<float> &matrix_a, std::vector<float> &matrix_b, std::vector<float> &matrix_c, size_t N, size_t M) {
std::cout << "Configuration : MATRIX_SIZE= " << N << "x" << N << "\n";
//# 步骤1:分配内存。
//# 在这一步,使用SYCL的buffer类在主机端为输入矩阵 A、B 和输出矩阵 C 分配内存。
buffer a(matrix_a);
buffer b(matrix_b);
buffer c(matrix_c);
//# 提交要在设备上执行的命令组
auto e = q.submit([&](handler &h){
//# 步骤2:数据传输
//# 这里创建了三个访问器,分别对应于矩阵 A、B 和 C。这些访问器会在设备端访问对应的缓冲区。通过这些访问器,数据会在主机和设备之间传输。
auto A = a.get_access<access::mode::read>(h);
auto B = b.get_access<access::mode::read>(h);
auto C = c.get_access<access::mode::write>(h);
//# 步骤3和步骤4:核函数调用和并行计算矩阵乘法
h.parallel_for(range<2>{
N,N}, [=](item<2> item){
const int i = item.get_id(0);
const int j = item.get_id(1);
for (int k = 0; k < N; k++) {
C[i*N+j] += A[i*N+k] * B[k*N+j];
}
});
});
//# 步骤5:数据传输
//# 确保在主机端等待内核执行完成,以便后续读取计算结果。这样就完成了数据的传输,可以在主机端访问计算后的输出矩阵。
c.get_access<access::mode::read>();
//# print kernel compute duration from event profiling
auto kernel_duration = (e.get_profiling_info<info::event_profiling::command_end>() - e.get_profiling_info<info::event_profiling::command_start>());
std::cout << "Kernel Execution Time : " << kernel_duration / 1e+9 << " seconds\n";
}
スクリプトを実行して計算する
#!/bin/bash
source /opt/intel/inteloneapi/setvars.sh > /dev/null 2>&1
# Command Line Arguments
arg=" -n 1024" # set matrix size
src="lab/"
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/intel/oneapi/compiler/latest/linux/lib
echo ====================
echo mm_dpcpp_basic
dpcpp ${src}mm_dpcpp_basic.cpp ${src}mm_dpcpp_common.cpp -o ${src}mm_dpcpp_basic -w -O3 -lsycl
./${src}mm_dpcpp_basic$arg
4.3 分析 1: ルーフラインレポート
ここでは、データ マトリックス サイズが異なる 2 つの異なる GPU ハードウェアについてレポートしました ( 1024 × 1024 1024\times 1024 1024×1024、 5120 × 5120 5120\times 5120 5120×5120、 10240 × 10240 10240\times 10240 10240×10240) マトリックス計算用のルーフライン ダイアグラム。
4.3.1 ルーフライン図の概要
ルーフライン チャートは、パフォーマンス分析に使用されるチャートの一種で、特にハイパフォーマンス コンピューティングの分野で広く使用されています。多くの場合、アルゴリズムのパフォーマンスを視覚化し、開発者がパフォーマンスのボトルネックや最適化の機会を特定するのに役立ちます。
ルーフライン チャートの横軸は通常、浮動小数点パフォーマンス (通常は FLOP/秒で測定) を表し、縦軸はパフォーマンス効率 (通常はワットあたりのパフォーマンスで測定) を表します。通常、図には 2 つの主要な部分が含まれます。
-
Roofline 带(Roof)
:ハードウェア性能の上限を示します。このバンドの形状は通常「屋根」であるため、ルーフライン ダイアグラムと呼ばれます。通常、帯域の左側はメモリ帯域幅の上限を表し、右側はコンピューティング パフォーマンスの上限を表します。 -
数据点
: パフォーマンス効率の空間におけるさまざまなアルゴリズムまたはアプリケーションの位置を示します。各データ ポイントは特定のアルゴリズムまたはアプリケーションに対応し、ポイントによって表されます。この点の横軸はアルゴリズムのパフォーマンスを表し、縦軸はパフォーマンス効率を表します。一般に、データ ポイントがルーフライン ゾーンに近づくほど、パフォーマンス効率が高くなることが期待されます。
この実験では、赤い点を使用して主要なデータ ポイントをマークし、基本的な SYCL カーネル並列行列計算のパフォーマンスと効率を示しています。
4.3.2 屋根線図の表示
(スペースの関係上、ここでは写真を 2 枚のみ掲載します)
4.3.3 ルーフライン図の分析
-
ルーフライン帯域に近い: 基本的な SYCL ベースの並列行列乗算のパフォーマンスはルーフライン帯域から遠く離れており、ハードウェア パフォーマンスの限界に達していないことを示しています。つまり、最適化の余地がまだ残っています。
-
縦軸の位置: 縦軸の位置はパフォーマンス効率を表します。図の結果によると、データ ポイントの位置はより低い。次のいくつかの最適化アルゴリズムでは、パフォーマンス効率を向上させるためにデータ ポイントをより高い位置に移動できることが期待されます。
-
横軸の位置: 横軸の位置はアルゴリズムのパフォーマンスを示します。図の結果によれば、横軸の値が以下では、いくつかの最適化アルゴリズムの中で、データ ポイントの横軸を大きくすること、つまりアルゴリズムのパフォーマンスを向上させ、ハードウェア パフォーマンスの上限に近づけることが望まれます。
4.4 分析 2: VTune™ プロファイラー分析
このセクションでは、Intel VTune Profiler をパフォーマンス分析に使用します。
4.4.1 インテル® VTune プロファイラーの概要
インテル® VTune プロファイラーは、開発者がアプリケーションのパフォーマンスのボトルネックを見つけて解決するのに役立つ詳細なパフォーマンス分析を提供します。インテル® VTune プロファイラーは、サマリー分析を含むさまざまな分析機能をサポートしています。
インテル® VTune プロファイラーの概要分析は、アプリケーションの全体的なパフォーマンスの高レベルの概要を提供します。概要分析から得られる情報は次のとおりです。
-
全体の概要: インテル® VTune プロファイラーの概要ページには、通常、合計実行時間、CPU 使用率、メモリ使用量、その他の情報を含む、アプリケーションの全体的なパフォーマンスの概要が表示されます。
-
ホットスポット: ホットスポットは、実行に最も時間がかかるアプリケーション内のコード領域です。概要分析では多くの場合、これらのホットスポットがリストされるため、開発者はどの部分が最も時間がかかっているかを迅速に特定できます。
-
関数レベルのパフォーマンス データ: 概要分析では、各関数の実行時間、呼び出し数、その他の情報を含む関数レベルのパフォーマンス データも提供される場合があります。これは、開発者が最適化が必要な特定の機能を見つけるのに役立ちます。
-
ハードウェア イベント: インテル® VTune プロファイラーは、キャッシュ・ヒット率、命令実行数などのハードウェア・イベントに関連する情報を収集できます。この情報は、プログラムのパフォーマンス特性をハードウェア レベルで把握するのに役立ちます。
4.4.2 インテル® VTune プロファイラーの結果表示
(スペースに限りがあるため、ここでは 4 枚の写真のみを掲載します)
4.4.3 インテル® VTune™ プロファイラーの分析結果
上の表の結果によれば、異なるハードウェアでの動作効率の比較 (同じデータ量では GPU の方が CPU より速い) と、データ量が動作効率に与える影響を観察できます。しかし全体としては、運用効率の最適化と改善の余地がまだ残っています。
4.5 まとめ: 数学カーネルライブラリの速度との比較
基本実験部分では、異なる行列サイズでの SYCL を使用した実行時間を実現しています。Intel が公開している数学カーネル ライブラリ時間と比較すると、小さな行列 (1024x1024) では基本的な性能が劣っていることがわかります。 SYCL 実装の方が優れており、MKL 実装をベースにしていますが、マトリックス サイズが増加すると、Intel によって測定された MKL 実装のパフォーマンスが SYCL 実装のパフォーマンスよりも優れています。
以下の 2 つの図では、2 つの GPU (GPU Gen9 および GPU Iris XE Max) でテスト マトリックス サイズが 1024x1024、5120x5120、および 10240x10240 の場合、異なるハードウェアでの実行時間は次のように描画されます。
1. GPU Gen9 の比較結果:
2. GPU Iris XE Maxの比較結果
5. 実装 2: ND-Range カーネルに基づくパフォーマンスの最適化
5.1 アルゴリズムの説明
5.1.1 ND 範囲のカーネルの説明
以前の基本的なコンピューティング手法では、ハードウェア レベルでパフォーマンスの最適化を実行できませんでした。このセクションでは、ND 範囲カーネルを使用して並列処理を表現し、グローバルおよびローカル メモリへのアクセスと、ハードウェア上の計算ユニットへのマッピング実行を提供することで、低レベルのパフォーマンス チューニングを可能にします。このプロセスでは、反復空間全体がワーク グループと呼ばれる小さなグループに分割されます。作業項目はこれらの作業グループに編成され、ハードウェア上の個々のコンピューティング ユニットでスケジュールされます。
ワークグループのサイズは、すべての次元で反復空間全体のサイズを正確に分割する必要があります。これらのサイズはハードウェア プラットフォームによって異なり、可能なサイズは以下のデバイス クエリを使用して決定できます。最適な組み合わせを見つけるには、開発者はワークロードを考慮する必要があります。
カーネルの実行をワークグループにグループ化すると、リソースの使用量を制御し、作業分散の負荷を分散するのに役立ちます。 nd_range
クラスは、ワークグループごとにグローバル実行スコープとローカル実行スコープを使用して、グループ化された実行スコープを表します。 nd_item
クラスは、ワークグループの範囲とインデックスのクエリを可能にするカーネル関数の単一インスタンスを表します。
5.1.2 ND-Range カーネルに基づく行列乗算アルゴリズムの説明
次の例では、ND-Range カーネルを使用して行列の乗算を計算する方法を示します。ワークグループ サイズはアクセラレータのハードウェア機能に依存するため、コマンド ライン パラメーターを使用してワークグループ サイズを設定します。一部のハードウェアでは、マトリックス サイズがワーク グループ サイズと等しい必要があります。この実験で使用されるデフォルトのワーク グループ サイズは、テストするすべてのアクセラレータ ハードウェアに適した 16x16 (256) です。
マトリックス計算アルゴリズムの補足図は次のとおりです。
5.2 アルゴリズムの実装
アルゴリズムの実装手順は次のとおりです。
-
mm_kernel
関数は、行列乗算のコア計算ロジックを定義し、SYCL のqueue
オブジェクトにカプセル化します。この関数は、キューq
と 3 つの行列matrix_a
、matrix_b
、matrix_c
を受け入れます。マトリックスのサイズN
とワークグループのサイズM
。 -
3 つのバッファ、、
buffer
クラスを通じて作成されます > 、デバイス上で入力および出力データを保存するために使用されます。ここで、 、、 はそれぞれ入力行列 A、B、出力行列 C のデータです。a
b
c
matrix_a
matrix_b
matrix_c
-
queue::submit
を使用してコマンド グループをデバイスに送信します。このコマンド グループでは、handler
オブジェクトh
が定義されており、デバイスと対話するためのさまざまな操作を設定するために使用されます。 -
コマンド グループ内で、アクセサ 、、< は最初に
get_access
a i によって作成されました。 =4>、デバイス上のバッファ内のデータにアクセスするために使用されます。 は、対応するアクセサーが読み取り専用であることを意味し、 は、対応するアクセサーが書き込み可能であることを意味します。A
B
C
mode::read
mode::write
-
次に、
range<2>
タイプのglobal_size
とwork_group_size
が定義され、それぞれグローバル サイズとワークグループ サイズを表します。ここでは 2 次元の ND 範囲が使用されます。N
は行列の次元を表します。 -
parallel_for
では、nd_range<2>
を通じてグローバル サイズとワークグループ サイズが定義され、並列実行用のカーネルとしてラムダ関数が定義されます。このカーネルでは、各ワークアイテム (スレッド) がグローバル ID に基づいて行列の特定の要素を取得し、行列乗算の累積演算を実行します。 -
最後に、
get_profiling_info
を通じてカーネルの実行時間を取得し、出力します。
このコードの鍵となるのは、ND-Range の並列計算モデルの使用です。このモデルは、作業項目を行列のさまざまな要素にマッピングすることで行列乗算の並列計算を実装します。これにより、行列乗算の計算パフォーマンスが大幅に向上します (特に大規模行列の場合)。
%%writefile lab/mm_dpcpp_ndrange.cpp
//==============================================================
// 矩阵乘法:SYCL ND-Range
// 版权归属:Copyright © 2021 Intel Corporation
//==============================================================
#include <CL/sycl.hpp>
using namespace sycl;
// SYCL ND-Range 矩阵乘法内核函数
void mm_kernel(queue &q, std::vector<float> &matrix_a, std::vector<float> &matrix_b, std::vector<float> &matrix_c, size_t N, size_t M) {
std::cout << "配置 : MATRIX_SIZE= " << N << "x" << N << " | WORK_GROUP_SIZE= " << M << "x" << M << "\n";
//# 为矩阵创建缓冲区
buffer a(matrix_a);
buffer b(matrix_b);
buffer c(matrix_c);
//# 提交命令组以在设备上执行
auto e = q.submit([&](handler &h){
//# 创建访问器以将缓冲区复制到设备
auto A = a.get_access<access::mode::read>(h);
auto B = b.get_access<access::mode::read>(h);
auto C = c.get_access<access::mode::write>(h);
//# 定义 ND-Range 和工作组大小
range<2> global_size(N,N);
range<2> work_group_size(M,M);
//# 并行计算矩阵乘法
h.parallel_for(nd_range<2>{
global_size, work_group_size}, [=](nd_item<2> item){
const int i = item.get_global_id(0);
const int j = item.get_global_id(1);
for (int k = 0; k < N; k++) {
C[i*N+j] += A[i*N+k] * B[k*N+j];
}
});
});
c.get_access<access::mode::read>();
//# 从事件分析中打印内核计算持续时间
auto kernel_duration = (e.get_profiling_info<info::event_profiling::command_end>() - e.get_profiling_info<info::event_profiling::command_start>());
std::cout << "内核执行时间 : " << kernel_duration / 1e+9 << " 秒\n";
}
5.3 分析 1: ルーフラインレポート
ここでは、データ マトリックス サイズが異なる 2 つの異なる GPU ハードウェアについてレポートしました ( 1024 × 1024 1024\times 1024 1024×1024、 5120 × 5120 5120\times 5120 5120×5120、 10240 × 10240 10240\times 10240 10240×10240)的Roofline图。
5.3.1 屋根線図の表示
(スペースの関係上、ここでは写真を 2 枚のみ掲載します)
5.3.2 ルーフライン図の分析
-
Roofline に近い: ND-Range カーネルに基づくパフォーマンスの最適化は、SYCL の基本的な並列行列乗算と交差します。パフォーマンスは Roofline に近くなり、向上していることを示しています。しかし、最適化の余地はまだあります。
-
縦軸の位置: 縦軸の位置はパフォーマンス効率を表します。図の結果によると、データ ポイントの位置が大幅に異なることがわかります。基本アルゴリズムに比べて改善されました。
-
横軸の位置: 横軸の位置はアルゴリズムのパフォーマンスを示します。図の結果によれば、アルゴリズムのパフォーマンスが、基本的な並列計算アルゴリズムでは、データ ポイントが右にシフトしていることは明らかな改善を示しています。
5.4 分析 2: VTune™ プロファイラー分析
このセクションでは、Intel VTune Profiler をパフォーマンス分析に使用します。
5.4.1 インテル® VTune プロファイラーの結果表示
(スペースに限りがあるため、ここでは 4 枚の写真のみを掲載します)
5.4.2 インテル® VTune プロファイラーの結果分析
上記の結果によると、マトリックスとワークグループのサイズの両方が、さまざまなプラットフォームでのカーネルのパフォーマンスに影響を与えます。ただし、同じ条件下では、基本アルゴリズムと比較して、さまざまなハードウェアでのパフォーマンスが向上し、より優れた最適化が達成されており、配列サイズが大きくなるほど、その効果はより顕著になります。
5.5 最適化された設計
ループの中間結果をカーネルに書き込むことができる場合、この変数はアクセラレータ ハードウェアのレジスタに変換されます。これにより、グローバル メモリへの書き込み回数が最小限に抑えられ、各ワークグループが結果を 1 回書き戻すだけになるため、パフォーマンスが向上します。コードの実行の簡単なデモンストレーションを次に示します。
%%writefile lab/mm_dpcpp_ndrange_var.cpp
//==============================================================
// 矩阵乘法:SYCL ND-range 私有内存
// 版权归属:Copyright © 2021 Intel Corporation
//==============================================================
#include <CL/sycl.hpp>
using namespace sycl;
void mm_kernel(queue &q, std::vector<float> &matrix_a, std::vector<float> &matrix_b, std::vector<float> &matrix_c, size_t N, size_t M) {
std::cout << "配置信息 : MATRIX_SIZE= " << N << "x" << N << " | WORK_GROUP_SIZE= " << M << "x" << M << "\n";
//# 为矩阵创建缓冲区
buffer a(matrix_a);
buffer b(matrix_b);
buffer c(matrix_c);
//# 提交命令组以在设备上执行
auto e = q.submit([&](handler &h){
//# 创建访问器以将缓冲区复制到设备
auto A = a.get_access<access::mode::read>(h);
auto B = b.get_access<access::mode::read>(h);
auto C = c.get_access<access::mode::write>(h);
//# 定义 ND-Range 和工作组大小
range<2> global_size(N, N);
range<2> work_group_size(M, M);
//# 并行计算矩阵乘法
h.parallel_for(nd_range<2>{
global_size, work_group_size}, [=](nd_item<2> item){
const int i = item.get_global_id(0);
const int j = item.get_global_id(1);
//# 使用私有内存存储中间结果
float temp = 0.f;
for (int k = 0; k < N; k++) {
temp += A[i * N + k] * B[k * N + j];
}
C[i * N + j] = temp;
});
});
c.get_access<access::mode::read>();
//# 从事件分析中打印内核计算持续时间
auto kernel_duration = (e.get_profiling_info<info::event_profiling::command_end>() - e.get_profiling_info<info::event_profiling::command_start>());
std::cout << "内核执行时间 : " << kernel_duration / 1e+9 << " 秒\n";
}
5.6 概要
ND-Range アルゴリズムを使用して並列行列乗算を実装することは、並列行列乗算を直接実行するよりも効果的です。主な理由は次のとおりです。
-
並列処理のより柔軟な制御: ND-Range モデルにより、グローバル サイズとワークグループ サイズをより柔軟に制御できます。これらのパラメーターを合理的に設定すると、さまざまなハードウェア アーキテクチャやマトリックス サイズに適切に適応できます。この柔軟性により、ハードウェアの並列処理を最大限に活用してパフォーマンスを向上させることができます。
-
さまざまなデバイスに適応: ND レンジ モデルは、マルチコア CPU、GPU、アクセラレータなど、さまざまなタイプの並列ハードウェア向けに設計されています。 ND-Range を使用すると、さまざまな種類のデバイスで実行できるより一般的なコードを作成し、それらのデバイスの並列処理をより効果的に活用できます。
-
負荷分散: ND 範囲モデルは、より優れた負荷分散の実現に役立ちます。各作業項目はマトリックス内の異なる場所で実行されるため、作業項目間での不均一な負荷分散を避けることができます。対照的に、単純な並列モデルでは、一部のワークグループが大量のデータを処理する一方で、他のワークグループが少量のデータを処理することになり、負荷の不均衡が生じる可能性があります。
-
データの局所性: ND-Range モデルは、データの局所性を活用するのに役立ちます。各作業項目はマトリックス内の特定の要素のみに焦点を当てます。これにより、キャッシュ ヒット率が向上し、メモリ アクセスの待ち時間が短縮され、パフォーマンスが向上します。
-
ハードウェア レベルの最適化: ND 範囲モデルを使用すると、コンパイラとハードウェアで追加の最適化を実行できます。これは、ND-Range がより多くの情報を提供し、コンパイラーとハードウェアが作業項目間の依存関係と並列性をよりよく理解できるため、より適切な命令スケジューリングとリソース割り当てが可能になるためです。
全体として、ND-Range モデルはより高いレベルの抽象化を提供し、並列アルゴリズムの実装をより柔軟かつ移植可能にし、並列ハードウェアのパフォーマンスをより有効に活用するのに役立ちます。よりよい性能。
また、プライベートメモリを使用する方法の効果を通常の ND-Range 方法と比較した実行結果は次のとおりです。
1. GPU Gen9 の比較結果:
2. GPU Iris XE Max の比較結果
グラフの結果によると、プライベート メモリを使用した後、同じハードウェア (GPU) および同じマトリックスで、サイズ、プライベートメモリの最適化が導入され、その後、効果がさらに向上しました。
6. 実装 3: 最適化された並列行列乗算のローカル メモリ実装
6.1 共有ローカル メモリ (SLM) の最適化の説明
-
データはローカル メモリにロードされます: 行列乗算のデータは計算で高度に再利用できます。したがって、アルゴリズムの最初のステップは、計算対象の行列 A と B を各ワークグループのローカル メモリにロードすることです。これにより、ワーク グループ内のワークアイテムがより速くデータにアクセスできるようになり、グローバル メモリからデータをロードする待ち時間が短縮されます。
-
ローカル メモリのライフ サイクル: ローカル メモリは各ワーク グループの開始時に初期化され、ワーク グループの実行終了後に破棄されます。これは、ローカル メモリは主にワークグループの実行中にデータを一時的に保存するために使用され、ワークグループ間で共有されないことを意味します。
-
ローカル アクセサーの使用: ローカル メモリを宣言するために、ローカル アクセサーが導入されます。説明で言及されている A_tile と B_tile はそのようなローカル アクセサーであり、16x16 データ ブロックをロードするために使用されます。これらのローカル アクセサーは中間結果を計算する役割を果たし、これらの中間結果ではグローバル メモリ計算を繰り返し使用する必要はありません。
-
ワークグループ内通信の最適化: ワークグループ内通信を高速化するために、アルゴリズムはワークグループ内通信専用のローカル メモリ空間を導入します。この特別なローカル メモリ空間は、カーネル開発を簡素化し、ワーク グループ内のワークアイテム間の通信効率を向上させるのに役立ちます。
-
バリアの導入: ワーク グループ内のすべての作業項目がローカル メモリの読み取りおよび書き込み操作を確実に完了するために、バリアが導入されます。これは、ワークグループ内の作業項目を同期して、正しい計算結果を保証するのに役立ちます。
-
パフォーマンスの向上: このアルゴリズムにより、元の ND 範囲サンプルとローカル メモリを使用した ND 範囲サンプルに比べてパフォーマンスが向上します。特に多くの GPU デバイスなどの一部のデバイスでは、ローカル メモリは特殊なリソースであるため、グローバル メモリを介した通信よりもローカル メモリを介した通信の方が効率的である場合があります。
要約すると、ローカル メモリと対応する最適化手法をアルゴリズムに導入することにより、行列乗算の並列計算パフォーマンスを効果的に向上させることができます。
6.2 コードの実装
%%writefile lab/mm_dpcpp_localmem.cpp
//==============================================================
// Matrix Multiplication: SYCL Local Accessor
// 版权归属:Copyright © 2021 Intel Corporation
//==============================================================
#include <CL/sycl.hpp>
using namespace sycl;
void mm_kernel(queue &q, std::vector<float> &matrix_a, std::vector<float> &matrix_b, std::vector<float> &matrix_c, size_t N, size_t M) {
std::cout << "Configuration : MATRIX_SIZE= " << N << "x" << N << " | WORK_GROUP_SIZE= " << M << "x" << M << "\n";
//# Create buffers for matrices
buffer a(matrix_a);
buffer b(matrix_b);
buffer c(matrix_c);
//# Submit command groups to execute on device
auto e = q.submit([&](handler &h){
//# Create accessors to copy buffers to the device
auto A = a.get_access<access::mode::read>(h);
auto B = b.get_access<access::mode::read>(h);
auto C = c.get_access<access::mode::write>(h);
//# Define size for ND-range and work-group size
range<2> global_size(N,N);
range<2> work_group_size(M,M);
//# Create local accessors
accessor<float, 2, access::mode::read_write, access::target::local> A_tile(range<2>(M, M), h);
accessor<float, 2, access::mode::read_write, access::target::local> B_tile(range<2>(M, M), h);
//# Parallel Compute Matrix Multiplication
h.parallel_for(nd_range<2>{
global_size, work_group_size}, [=](nd_item<2> item){
const int i = item.get_global_id(0);
const int j = item.get_global_id(1);
const int x = item.get_local_id(0);
const int y = item.get_local_id(1);
float temp = 0.f;
int k;
for (int t = 0; t < N; t+=M) {
A_tile[x][y] = A[i * N + (t + y)];
B_tile[x][y] = B[(t + x) * N + j];
item.barrier(access::fence_space::local_space);
for (k = 0; k < M; k++) {
temp += A_tile[x][k] * B_tile[k][y];
}
item.barrier(access::fence_space::local_space);
}
C[i*N+j] = temp;
});
});
c.get_access<access::mode::read>();
//# print kernel compute duration from event profiling
auto kernel_duration = (e.get_profiling_info<info::event_profiling::command_end>() - e.get_profiling_info<info::event_profiling::command_start>());
std::cout << "Kernel Execution Time : " << kernel_duration / 1e+9 << " seconds\n";
}
実行スクリプトは次のように実行されます。
6.3 分析 1: ルーフラインレポート
ここでは、データ マトリックス サイズが異なる 2 つの異なる GPU ハードウェアについてレポートしました ( 1024 × 1024 1024\times 1024 1024×1024、 5120 × 5120 5120\times 5120 5120×5120、 10240 × 10240 10240\times 10240 10240×10240)的Roofline图。
6.3.1 屋根線図の表示
(スペースの関係上、ここでは写真を 2 枚のみ掲載します)
6.3.2 ルーフライン図の分析
-
ルーフラインに近い: 共有ローカル メモリ (SLM) 最適化アルゴリズムを使用すると、前のセクションの ND-Range カーネルに基づくパフォーマンスの最適化と比較して、パフォーマンスがより近くなります。最近では改善されていることが示されていますが、まだ最適化の余地があります。
-
縦軸の位置: 縦軸の位置はパフォーマンス効率を表します。図の結果によると、データ ポイントの位置が調整されていることがわかります。前述のアルゴリズムと比較して大幅に改善されました。
-
横軸の位置: 横軸の位置はアルゴリズムのパフォーマンスを表します。図の結果によると、前述の並列計算アルゴリズムでは、データ ポイントが右に大きくシフトしていることは、大幅な改善を示しています。
6.4 分析 2: VTune™ プロファイラー分析
このセクションでは、Intel VTune Profiler をパフォーマンス分析に使用します。
6.4.1 インテル® VTune プロファイラーの結果表示
(スペースに限りがあるため、ここでは 4 枚の写真のみを掲載します)
6.4.2 インテル® VTune プロファイラーの結果分析
結果によると、同じハードウェア条件とマトリックス サイズの下で、共有ローカル メモリ手法を使用して最適化した後、以前のいくつかのアルゴリズムと比較して全体の実行速度が向上しました。
6.5 分析
ここでは、このアルゴリズムを以前の最適な ND 範囲ベースのプライベート メモリ最適化アルゴリズムと比較し、異なるハードウェアとマトリックス サイズで視覚化した後の結果は次のとおりです。
1. GPU Gen9 の比較結果:
2. GPU Iris XE Maxの比較結果
比較すると、SLM アルゴリズムを使用した後に達成される行列乗算のパフォーマンスが向上していることがわかります。
7. まとめと感想
上記の 3 つの方法と Intel の MKL を大きなマトリックス (20480*20480) で比較し、それぞれ 4 台のデバイスでテストし、最終的な最適化比較グラフは次のようになります。
この実験では、学習は主に Intel が提供する公式の並列行列計算ガイダンスに基づいて行われました。アルゴリズムを段階的に最適化することにより、行列乗算計算の演算効果も徐々に向上していることがわかります。この研究を通じて、並列アルゴリズムの実装についての理解を深め、学習に基づいて並列ソートアルゴリズムと並列ソートアルゴリズムをさらに実装していきます。