本記事はNVIDIAの協力に基づいて記事作成しております。
データ転送を速くする
前回のサンプル:1000×1000行列のかけ算の実行結果を再掲します。
簡単なサンプルとして行列積を取り上げましたけど、実アプリではこんなのわざわざ実装する必要はありません。基本的な線型代数ルーチンを集めたBLAS(Basic Linear Algebra Subprograms)のCUDA実装:cuBLASがCUDA Toolkitに含まれています。デバイス側コードはcuBLASライブラリの中にあるので、.cuを書くこともなく通常のC/C++ライブラリとしてリンクし、利用できます(cuBLASによる行列積をサンプルファイルに納めました)。
デバイス内の処理は20GFLOPS超のナイスなスピードを叩き出しています。が、ホスト-デバイス間のデータ転送を含めると……ちょっと残念。データ転送時間が足を引っ張っています。PCI-busを越えて大量のデータをやりとりするのはそれなりの時間がかかるものです。
ホスト-デバイス間のデータ転送はDMA(Direct Memory Access)を用い、CPUの動作とは独立してホスト側のメモリとビデオ・メモリとの間でコピーを行っています。ホスト側プログラム(プロセス)は仮想アドレス空間で動いており、プログラム内のポインタが指す値(アドレス)と実メモリ上の物理アドレスは一致しません。さらにOSはメモリの使用状況を監視していて、必要に応じてメモリの内容をディスクに保存し(ページアウト)、他の目的で使ったりします。
DMAにしてみればページアウト領域がコピー元/先では意図しない値がコピーされることとなり、困ってしまいます。そこで物理メモリ上に特定の固定領域を用意し、コピー元からいったんそちらに転送したのちにDMAがデバイスにコピーを行います(逆方向も同様)。CUDAではページアウトしない"ピン留め"された領域(page-lockedメモリ/pinnedメモリ)を確保するAPIが用意されています。ピン留めされた領域なら、そこから直接DMA転送できるってスンポーです。
page-lockedメモリの確保は簡単、cudaMemcpyによるホスト-デバイス間コピーの対象となるホスト側領域をcudaMallocHostで確保するだけ。
型* p = new 型[要素数];
や
型* p = (型*)malloc(要素数*sizeof(型));
を
型* p = nullptr; cudaMallocHost(&p, 要素数*sizeof(型));
に置き換え、delete[] p;やfree(p)はcudaFreeHost(p)に改めます。
早速実験。10メガbyteのデータをホスト-デバイス間で100往復させ、所要時間を測ります。
#include "cuda_runtime.h"
#include <iostream>
#include <chrono>
#include <cassert>
using namespace std;
chrono::milliseconds memory_test(char* host, char* device, size_t size, unsigned int repeat) {
chrono::high_resolution_clock::time_point start = chrono::high_resolution_clock::now();
while ( --repeat ) {
cudaMemcpy(device, host, size, cudaMemcpyHostToDevice);
cudaMemcpy(host, device, size, cudaMemcpyDeviceToHost);
}
chrono::high_resolution_clock::time_point stop = chrono::high_resolution_clock::now();
return chrono::duration_cast<chrono::milliseconds>(stop - start);
}
int main() {
const size_t size = 1024*1024*10; // 10M-bytes
const unsigned int repeat = 100; // 100 times
char* device;
cudaMalloc(&device, size);
chrono::milliseconds duration;
char* host = nullptr;
for ( int i = 0; i < 5; ++i ) {
cout << "--- normal -------- ";
host = new char[size];
assert( host != nullptr );
duration = memory_test(host, device, size, repeat);
long long nms = duration.count();
cout << duration.count() << " [ms]" << endl;
delete[] host;
host = nullptr;
cout << "--- page-locked --- ";
cudaError_t status = cudaMallocHost(&host, size);
assert( status == cudaSuccess );
duration = memory_test(host, device, size, repeat);
long long pms = duration.count();
cout << duration.count() << " [ms] (" << pms * 1.0f / nms << ")" << endl;
cudaFreeHost(host);
host = nullptr;
cout << endl;
}
return 0;
}
40%ほど時間短縮できているみたいです。この差は決して小さくないですよ。
ホスト-デバイス間のデータ転送はその量/回数ともにできるだけ少なくなるような実装設計がパフォーマンスのキモとなりそうです。いったんホストからデバイスにデータを投げたら、極力デバイス内で処理を完結し、最終結果のみをデバイスから引き取るのがベストでしょうね。

