異種メモリ管理により GPU アプリケーション開発を簡素化
ヘテロジニアス メモリ管理 (HMM) は、CUDA ユニファイド メモリ プログラミング モデルのシンプルさと生産性を拡張し、PCIe 接続された NVIDIA GPU を備えたシステム上でシステム割り当てメモリを含める CUDA メモリ管理機能です。システム割り当てメモリとは、オペレーティング システムによって最終的に割り当てられるメモリです。たとえば、malloc、mmap、C++ new 演算子 (もちろん前述のメカニズムを使用)、またはアプリケーション用に CPU がアクセスできるメモリを設定する関連システム ルーチンを介して割り当てられます。
以前は、PCIe ベースのコンピューターでは、GPU はシステムによって割り当てられたメモリに直接アクセスできませんでした。GPU は、cudaMalloc や cudaMallocManated などの特殊なアロケーターからのみメモリにアクセスできます。
HMM が有効な場合、すべてのアプリケーション スレッド (GPU または CPU) は、アプリケーションのシステム割り当てメモリのすべてに直接アクセスできます。ユニファイド メモリ (HMM のサブセットまたは前身と考えることができます) と同様に、システムによって割り当てられたメモリをプロセッサ間で手動でコピーする必要はありません。これは、プロセッサーの使用状況に基づいて CPU または GPU に自動的に配置されるためです。
CUDA ドライバー スタックでは、メモリを配置すべき場所を検出するために、CPU および GPU のページ フォールトが一般的に使用されます。繰り返しますが、この自動配置はすでにユニファイド メモリ内で行われています。HMM は動作を拡張して、システム割り当てメモリと cudaMalloc 管理メモリをカバーするだけです。
アプリケーション メモリ アドレス空間全体を直接読み書きできるこの新機能により、CUDA 上に構築されたすべてのプログラミング モデル (CUDA C++、Fortran、Python の標準並列処理、ISO C++、ISO Fortran、OpenACC、OpenMP など) のプログラマの生産性が大幅に向上します。 。
実際、次の例が示すように、HMM は GPU プログラミングを簡素化し、GPU プログラミングを CPU プログラミングとほぼ同じくらいアクセスしやすくします。いくつかのハイライト:
- GPU プログラムを作成する場合、関数は明示的なメモリ管理を必要としないため、最初の「最初のドラフト」プログラムは小さくて単純なものにすることができます。明示的メモリ管理 (パフォーマンス チューニングのため) は、開発の後の段階まで延期できます。
- GPU プログラミングは、CPU メモリと GPU メモリを区別しないプログラミング言語で実用的になりました。
- 大規模なアプリケーションは、大規模なメモリ管理リファクタリングやサードパーティ ライブラリ (ソース コードが常に利用できるとは限らない) への変更を行わなくても、GPU によって高速化できます。
ちなみに、NVIDIA Grace Hopper などの新しいハードウェア プラットフォームは、すべての CPU と GPU 間のハードウェア ベースのメモリ一貫性を備えたユニファイド メモリ プログラミング モデルをネイティブにサポートしています。このようなシステムの場合、HMM は必要ありません。実際、そこでは HMM が自動的に無効になります。これについて考える 1 つの方法は、HMM が実際にはソフトウェア ベースのアプローチであり、NVIDIA Grace Hopper スーパーチップと同じプログラミング モデルを提供していることを観察することです。
HMM 以前のユニファイド メモリ
2013 年に導入されたオリジナルの CUDA ユニファイド メモリ機能を使用すると、次のようにいくつかの変更を加えるだけで CPU プログラムを高速化できます。
CPUモード
void sortfile(FILE* fp, int N) {
char* data;
data = (char*)malloc(N);
fread(data, 1, N, fp);
qsort(data, N, 1, cmp);
use_data(data);
free(data);
}
オリジナルのユニファイドメモリ呼び出し方式
void sortfile(FILE* fp, int N) {
char* data;
cudaMallocManaged(&data, N);
fread(data, 1, N, fp);
qsort<<<...>>>(data, N, 1, cmp);
cudaDeviceSynchronize();
use_data(data);
cudaFree(data);
}
このプログラミング モデルはシンプル、明確、そして強力です。過去 10 年間にわたり、このアプローチにより、数え切れないほどのアプリケーションが GPU アクセラレーションの恩恵を簡単に受けられるようになりました。ただし、まだ改善の余地があります。特別なアロケータ (cudaMallocManated および対応する cudaFree) が必要であることに注意してください。
さらに一歩進んで、これらを取り除くことができたらどうなるでしょうか? これはまさに HMM が行うことです。
HMM後のユニファイドメモリ
HMM を備えたシステム (詳細は下記) では、引き続き malloc と free を使用します。
CPUモード
void sortfile(FILE* fp, int N) {
char* data;
data = (char*)malloc(N);
fread(data, 1, N, fp);
qsort(data, N, 1, cmp);
use_data(data);
free(data);
}
最新のCUDAユニファイドメモリとHMM
void sortfile(FILE* fp, int N) {
char* data;
data = (char*)malloc(N);
fread(data, 1, N, fp);
qsort<<<...>>>(data, N, 1, cmp);
cudaDeviceSynchronize();
use_data(data);
free(data)
}
HMM を使用すると、メモリ管理は 2 つ間で同じになります。
システム割り当てメモリと CUDA アロケータ
CUDA メモリ アロケータを使用する GPU アプリケーションは、HMM を備えたシステム上で「そのまま」動作します。これらのシステムの主な違いは、malloc、C++ new、mmap などのシステム割り当て API が、CUDA API を呼び出してこれらの割り当てが存在することを CUDA に伝えることなく、GPU スレッドからアクセスできる割り当てを作成することです。次の表に、HMM を備えたシステム上の最も一般的な CUDA メモリ アロケータの違いを示します。
HMM を備えたシステムのメモリ アロケータ | 配置 | 移行可能 | からアクセス可能: | ||
CPU | GPU | RDMA | |||
システムが割り当てられましたmalloc 、、mmap … |
ファーストタッチ GPU または CPU |
Y | Y | Y | Y |
CUDA管理cudaMallocManaged |
Y | Y | Y | N | |
CUDA デバイスのみcudaMalloc 、… |
GPU | N | N | Y | Y |
CUDA ホスト固定cudaMallocHost , … |
CPU | N | Y | Y | Y |
一般に、CUDA は、アプリケーションの意図をより適切に表現するアロケーターを選択することにより、より良いパフォーマンスを提供します。HMM を使用すると、これらの選択はパフォーマンスの最適化となり、GPU から初めてメモリにアクセスする前に事前に実行する必要はありません。HMM を使用すると、開発者はまずアルゴリズムの並列化に集中し、オーバーヘッドによってパフォーマンスが向上したときにメモリ アロケータ関連の最適化を実行できます。
C++、Fortran、Python のシームレスな GPU アクセラレーション HMM により、
Python などの標準化された移植可能なプログラミング言語を使用して NVIDIA GPU をプログラミングすることが容易になります。Python では、CPU メモリと GPU メモリを区別せず、すべてのスレッドがすべてのメモリにアクセスできると想定されます。メモリ、および ISO Fortran や ISO C++ などの国際標準で記述されたプログラミング言語。
これらの言語は、実装で計算を GPU やその他のデバイスに自動的にディスパッチできるようにする同時実行機能と並列処理機能を提供します。たとえば、C++ 2017 以降、<algorithm>
ヘッダー ファイルの標準ライブラリ アルゴリズムは、実装で並列実行できるようにする実行ポリシーを受け入れます。
GPU からその場でファイルを並べ替える
たとえば、HMM が登場する前は、CPU メモリより大きいファイルのインプレース ソートは、最初にファイルの小さな部分をソートし、次にそれらを完全にソートされたファイルにマージするという複雑な作業でした。HMM を使用すると、アプリケーションは mmap を使用してディスク上のファイルをメモリにマップし、GPU から直接読み書きすることができます。詳細については、GitHub のHMM サンプル コードfile_before.cppおよびfile_after.cppを参照してください。
オリジナルの動的割り当て
void sortfile(FILE* fp, int N) {
std::vector<char> buffer;
buffer.resize(N);
fread(buffer.data(), 1, N, fp);
// std::sort runs on the GPU:
std::sort(std::execution::par,
buffer.begin(), buffer.end(),
std::greater{
});
use_data(std::span{
buffer});
}
最新のユニファイドメモリ+HMMの動的割り当て
void sortfile(int fd, int N) {
auto buffer = (char*)mmap(NULL, N,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// std::sort runs on the GPU:
std::sort(std::execution::par,
buffer, buffer + N,
std::greater{
});
use_data(std::span{
buffer});
}
-stdpar=gpu オプションが使用されている場合、並列 std::sort アルゴリズムのNVIDIA C++ コンパイラー(NVC++) 実装は GPU 上でファイルをソートします。このオプションの使用には多くの制限があります。詳細については、HPC SDK ドキュメントで説明されています。
HMM 以前: GPU は、NVC++ コンパイル済みコード内のヒープ上に動的に割り当てられたメモリにのみアクセスできます。つまり、CPU スレッド スタック上の自動変数、グローバル変数、およびメモリ マップされたファイルには GPU からアクセスできません (以下の例を参照)。
HMM 後: GPU は、他のコンパイラやサードパーティ ライブラリによってコンパイルされた CPU コード内のヒープ上に動的に割り当てられたデータ、CPU スレッド スタック上の自動変数、CPU メモリ内のグローバル変数、メモリ マッピング ドキュメントなど、システムによって割り当てられたすべてのメモリにアクセスできます。等
アトミックなメモリ操作と同期プリミティブ
HMM は、アトミック メモリ操作を含むすべてのメモリ操作をサポートします。つまり、プログラマはアトミック メモリ操作を使用して、GPU スレッドと CPU スレッドをフラグと同期させることができます。C++ std::atomic API の一部では、GPU ではまだ利用できないシステム コール ( std::atomic::waitやstd::atomic::notify_all/_one APIなど) を使用しますが、C++ 同時実行プリミティブのほとんどはAPI を使用して、GPU スレッドと CPU スレッドの間でメッセージ パッシングを実行することが簡単にできます。
詳細については、 GitHub の「HPC SDK C++ Parallel Algorithms: Interoperability with C++ Standard Library」ドキュメントとatomic_flag.cpp HMM サンプル コードを参照してください。このセットは CUDA C++ で拡張できます。詳細については、GitHub の Ticket_lock.cpp HMM サンプル コードを参照してください。
HMM CPU←→GPU メッセージパッシング前
void main() {
// Variables allocated with cudaMallocManaged
std::atomic<int>* flag;
int* msg;
cudaMallocManaged(&flag, sizeof(std::atomic<int>));
cudaMallocManaged(&msg, sizeof(int));
new (flag) std::atomic<int>(0);
*msg = 0;
// Start a different CPU thread…
auto t = std::jthread([&] {
// … that launches and waits
// on a GPU kernel completing
std::for_each_n(
std::execution::par,
&msg, 1, [&](int& msg) {
// GPU thread writes message…
*msg = 42; // all accesses via ptrs
// …and signals completion…
flag->store(1); // all accesses via ptrs
});
});
HMM CPU←→GPUメッセージパッシング後
void main() {
// Variables on CPU thread stack:
std::atomic<int> flag = 0; // Atomic
int msg = 0; // Message
// Start a different CPU thread…
auto t = std::jthread([&] {
// … that launches and waits
// on a GPU kernel completing
std::for_each_n(
std::execution::par,
&msg, 1, [&](int& msg) {
// GPU thread writes message…
msg = 42;
// …and signals completion…
flag.store(1);
});
});
// CPU thread waits on GPU thread
while (flag.load() == 0);
// …and reads the message:
std::cout << msg << std::endl;
// …the GPU kernel and thread
// may still be running here…
}
HMM CPU←→GPUロック前
void main() {
// Variables allocated with cudaMallocManaged
ticket_lock* lock; // Lock
int* msg; // Message
cudaMallocManaged(&lock, sizeof(ticket_lock));
cudaMallocManaged(&msg, sizeof(int));
new (lock) ticket_lock();
*msg = 0;
// Start a different CPU thread…
auto t = std::jthread([&] {
// … that launches and waits
// on a GPU kernel completing
std::for_each_n(
std::execution::par,
&msg, 1, [&](int& msg) {
// GPU thread takes lock…
auto g = lock->guard();
// … and sets message (no atomics)
msg += 1;
}); // GPU thread releases lock here
});
{
// Concurrently with GPU thread
// … CPU thread takes lock…
auto g = lock->guard();
// … and sets message (no atomics)
msg += 1;
} // CPU thread releases lock here
t.join(); // Wait on GPU kernel completion
std::cout << msg << std::endl;
}
HMM CPU←→GPUロック後
void main() {
// Variables on CPU thread stack:
ticket_lock lock; // Lock
int msg = 0; // Message
// Start a different CPU thread…
auto t = std::jthread([&] {
// … that launches and waits
// on a GPU kernel completing
std::for_each_n(
std::execution::par,
&msg, 1, [&](int& msg) {
// GPU thread takes lock…
auto g = lock.guard();
// … and sets message (no atomics)
msg += 1;
}); // GPU thread releases lock here
});
{
// Concurrently with GPU thread
// … CPU thread takes lock…
auto g = lock.guard();
// … and sets message (no atomics)
msg += 1;
} // CPU thread releases lock here
t.join(); // Wait on GPU kernel completion
std::cout << msg << std::endl;
}
HMM を使用して複雑な HPC ワークロードを高速化する
大規模で寿命の長い HPC アプリケーションに取り組んでいる研究グループは、異種プラットフォーム向けに、より効率的で移植可能なプログラミング モデルを提供することを長年熱望してきました。m-AIAは、ドイツのアーヘン工科大学空気力学研究所によって開発されたマルチフィジックス ソルバーで、約 300,000 行のコードが含まれています。詳細については、「OpenACCを使用した。初期のプロトタイプでは OpenACC は使用されなくなりましたが、前述の ISO C++ プログラミング モデルを使用して GPU 上で部分的に高速化されましたが、プロトタイプの作業が完了した時点ではこのモデルは利用できませんでした。
HMM を使用すると、私たちのチームは、初期条件と I/O のためにFFTWやpnetcdf などの GPU に依存しないサードパーティ ライブラリとインターフェイスし、GPU が同じメモリに直接アクセスすることを気にしない、新しい m-AIA ワークロードを高速化できます。
メモリマップド I/O による迅速な開発
HMM によって提供される興味深い機能は、GPU から直接メモリ マップされたファイル I/O を提供することです。これにより、開発者は、システム メモリにファイルをステージングしたり、高帯域幅の GPU メモリにデータをコピーしたりすることなく、サポートされているストレージまたはディスクからファイルを直接読み取ることができます。これにより、アプリケーション開発者は、反復的なデータ取り込みや計算ワークフローを構築することなく、利用可能な物理システム メモリよりも大きい入力データを簡単に処理できるようになります。
この機能を実証するために、私たちのチームは、 ERA5 再解析データセットから年間の各日の時間ごとの総降水量ヒストグラムを構築するサンプル アプリケーションを作成しました。詳細については、 「ERA5 グローバル再分析」を参照してください。
ERA5 データセットは、いくつかの大気変数の時間ごとの推定値で構成されています。データセットでは、各月の総降水量データが別のファイルに保存されます。1981 年から 2020 年までの 40 年間の総降水量データを使用しました。入力ファイルは合計 480 個、入力データの合計サイズは約 1.3 TB でした。結果の例については、下の画像を参照してください。
Unix mmap API を使用すると、入力ファイルを連続した仮想アドレス空間にマッピングできます。HMM を使用すると、この仮想アドレスを入力として CUDA カーネルに渡すことができ、CUDA カーネルはこれらの値に直接アクセスして、年間を通じて毎日の 1 時間ごとの総降水量のヒストグラムを作成できます。
結果のヒストグラムは GPU メモリに保存され、北半球の月平均降水量などの興味深い統計を簡単に計算するために使用できます。たとえば、2 月と 8 月の時間平均降水量も計算しました。このアプリケーションのコードを確認するには、 GitHub のHMM_sample_codeにアクセスしてください。
HMM バッチおよびパイプライン メモリ転送の前
size_t chunk_sz = 70_gb;
std::vector<char> buffer(chunk_sz);
for (fp : files)
for (size_t off = 0; off < N; off += chunk_sz) {
fread(buffer.data(), 1, chunk_sz, fp);
cudeMemcpy(dev, buffer.data(), chunk_sz, H2D);
histogram<<<...>>>(dev, N, out);
cudaDeviceSynchronize();
}
HMM 後 メモリ マップとオンデマンド転送
void* buffer = mmap(NULL, alloc_size,
PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
for (fd : files)
mmap(buffer+file_offset, fileByteSize,
PROT_READ, MAP_PRIVATE|MAP_FIXED, fd, 0);
histogram<<<...>>>(buffer, total_N, out);
cudaDeviceSynchronize();
HMM を有効にして検出する
CUDA ツールキットとドライバーは、システムがそれを処理できることを検出すると、自動的に HMM を有効にします。これらの要件については、「CUDA 12.2 リリース ノート: Universal CUDA」に詳しく記載されています。必要がある:
- オープンソース r535_00 ドライバーを備えた NVIDIA CUDA 12.2 以降。詳細については、NVIDIA Open GPU カーネル モジュールのインストール ドキュメントを参照してください。
- 十分に新しい Linux カーネル: 6.1.24 以降、6.2.11 以降、または 6.3 以降。
- サポートされているアーキテクチャのいずれかを備えた GPU: NVIDIA Turing、NVIDIA Ampere、NVIDIA Ada Lovelace、NVIDIA Hopper 以降。
- 64 ビット x86 CPU。
アドレッシング モードのプロパティをクエリして、HMM が有効になっていることを確認します。
$ nvidia-smi -q | grep Addressing
Addressing Mode : HMM
GPU がシステム割り当てメモリにアクセスできるシステムを検出するには、 をクエリしますcudaDevAttrPageableMemoryAccess
。
さらに、NVIDIA Grace Hopper Superchip などのシステムは、HMM のように動作する ATS をサポートしています。実際、HMM システムと ATS システムのプログラミング モデルは同じであるため、ほとんどのプログラムではチェックだけでcudaDevAttrPageableMemoryAccess
十分です。
ただし、パフォーマンス チューニングやその他の高度なプログラミングの場合、cudaDevAttrPageableMemoryAccessUsesHostPageTables
HMM と ATS を区別するためのクエリを作成することもできます。次の表は、結果を解釈する方法を示しています。
属性 | ふーむ | ATS |
---|---|---|
cudaDevAttrPageableMemoryAccess | 1 | 1 |
cudaDevAttrPageableMemoryAccessUsesHostPageTables | 0 | 1 |
HMM または ATS によって公開されるプログラミング モデルが利用可能かどうかをクエリすることのみに関心があるポータブル アプリケーションの場合は、通常、ページング可能メモリ アクセス属性をクエリするだけで十分です。
統合メモリのパフォーマンスに関するヒント
既存のユニファイド メモリ パフォーマンス ヒントのセマンティクスは変更されていません。NVIDIA Grace Hopper などのハードウェア コヒーレント システムで CUDA ユニファイド メモリをすでに使用しているアプリケーションの場合、主な変更点は、HMM により、上記の制約内でより多くのシステムでアプリケーションを「そのまま」実行できるようになることです。
既存の統合メモリのヒントは、HMM システム上のシステム割り当てメモリにも適用されます。
__host__ cudaError_t cudaMemPrefetchAsync(* ptr, size_t nbytes, int device):
メモリを GPU (GPU DeviceID) または CPU (cudaCpuDeviceId) に非同期的にプリフェッチします。__host__ cudaError_tcudaMemAdvise(*ptr, size_t nbytes, cudaMemoryAdvise,advice, int device)
: プロンプトシステム:
- メモリ内の優先場所:
cudaMemAdviseSetPreferredLocation
、または - メモリにアクセスするデバイス:
cudaMemAdviseSetAccessedBy
、または - 主に変更頻度の低いメモリを読み取るデバイス:
cudaMemAdviseSetReadMostly
。
もう少し高度な機能として、新しい CUDA 12.2 API であるcudaMemAdvise_v2があり、アプリケーションはこれを使用して、特定のメモリ範囲に対してどの NUMA ノードを優先するかを選択できます。これは、HMM がメモリの内容を CPU 側に配置するときに機能します。
いつものように、メモリ管理のヒントによりパフォーマンスが向上または低下する可能性があります。動作はアプリケーションとワークロードによって異なりますが、ヒントはアプリケーションの正確さに影響を与えるべきではありません。
CUDA 12.2 における HMM の制限
CUDA 12.2 の最初の HMM 実装では、既存のアプリケーションのパフォーマンスを低下させることなく新しい機能が提供されます。CUDA 12.2 の HMM の制限については、「CUDA 12.2 リリース ノート: CUDA 一般」に詳しく記載されています。主な制限は次のとおりです。
- HMM は x86_64 でのみ使用でき、他の CPU アーキテクチャはまだサポートされていません。
- HugeTLB 割り当て上の HMM はサポートされていません。
- ファイルバックアップ メモリおよび HugeTLBfs メモリに対する GPU アトミック操作はサポートされていません。
- 後続の exec(3) を伴わない fork(2) は完全にはサポートされていません。
- ページの移行は、4 KB のページ サイズのチャンクで処理されます。
HMM の制限に対処し、パフォーマンスを向上させる今後の CUDA ドライバーの更新にご期待ください。
要約する
HMM は、一般的な PCIe ベース (通常は x86) コンピューター上で実行される GPU プログラムの明示的なメモリ管理の必要性を排除することで、プログラミング モデルを簡素化します。プログラマは、CPU プログラミングの場合と同様に、malloc、C++ new、および mmap 呼び出しを直接使用できます。
HMM は、CUDA プログラムでさまざまな標準プログラミング言語機能を安全に使用することにより、プログラマの生産性をさらに高めます。システムによって割り当てられたメモリが誤って CUDA カーネルに公開されることを心配する必要はありません。
HMM により、新しい NVIDIA Grace Hopper スーパーチップおよび同様のマシンとの間でシームレスな移行が可能になります。PCIe ベースのマシンでは、HMM は、NVIDIA Grace Hopper スーパーチップで使用されているのと同じ簡素化されたプログラミング モデルを提供します。