何の話か
UnifiedメモリがCUDA 6にはじめて導入されしばらく経ちました。
これはメモリのpage faultを用いた自動メモリ転送機能で、cudaMallocManagedでDeviceメモリを確保することで利用できます。
何か既存のコードをGPU移植する場合などには重宝していますが、初めから生CUDAを書いている場合はなんとなく遅い気がして敬遠しがちです。
今回少し性能テストを行ったのでその結果をまとめてみました。
性能評価
今回私が知りたいのは次の2点です。
- cudaMalloc/cudaMallocHost/cudaMemcpyを合わせて用いた場合とcudaMallocManagedを用いた場合では、どちらの方が速くHost to Deviceのメモリコピーができるのか?
- cudaMallocManagedとcudaMallocではDeviceメモリ上のデータへのアクセス速度は異なるのか?
この2点を簡単なSgemmのコードを用いて評価しました。
コードの内容としては、cudaMalloc&cudaMallocHostを用いてメモリを確保した場合とcudaMallocManagedで確保した場合で、ホストで行列を初期化した後にGPU上でSgemmを計算するだけのものとなっています。
1. HtoDデータ転送「cudaMalloc&cudaMallocHost&cudaMemcpy」対「cudaMallocManaged」
上記の評価コードをtest_count=1で回します。
転送開始からSgemm完了までにかかった時間はそれぞれこの様になっています。
行列サイズが小さい時、すなわちHtoDのデータ転送量が小さいときはUnifiedメモリの方が時間がかかることが分かります。
これは、データ転送量は\(O(N^2)\)で増えるに対し行列積の計算量は\(O(N^3)\)で増えるので、Nが大きくなるのに従いUnifiedメモリによるメモリの転送の遅さが計算時間に対して相対的に小さくなっていると考えられます。
左端のN=32では10倍程度実行時間に差があります。
2. GPUからのメモリアクセス「cudaMalloc」対「cudaMallocManaged」
続いてメモリアクセスの速度の比較です。
こちらはCULiPを使ってcublasSgemmにかかった時間を測ることで評価します。
今回はtest_count=8として実行します。
この場合、Unifiedメモリでは1回目のSgemmの際にHtoD転送が起き、2~7回目ではデータはDeviceメモリ上にあるためHtoD転送は起きません。
ですので、今行いたいのは2~7回目のSgemmにかかった時間の比較です。
こちらがCULiPで計測したcublasSgemmの時間です。
- 普通のDeviceメモリを用いた場合
- cublasSgemm_v2 : [4000826546 ns; 4.000827e+00 s;100.00%] params count sum avg max min m16384-n16384-k16384 7 3475.566ms( 86.87%) 496.509ms 496.570ms 496.415ms m8192-n8192-k8192 7 441.875ms( 11.04%) 63.125ms 65.970ms 62.164ms m4096-n4096-k4096 7 73.810ms( 1.84%) 10.544ms 11.199ms 9.832ms m2048-n2048-k2048 7 7.414ms( 0.19%) 1.059ms 1.062ms 1.057ms m1024-n1024-k1024 7 1.450ms( 0.04%) 0.207ms 0.209ms 0.206ms m512-n512-k512 7 0.314ms( 0.01%) 0.045ms 0.046ms 0.044ms m256-n256-k256 7 0.140ms( 0.00%) 0.020ms 0.031ms 0.018ms m128-n128-k128 7 0.096ms( 0.00%) 0.014ms 0.015ms 0.013ms m64-n64-k64 7 0.088ms( 0.00%) 0.013ms 0.019ms 0.009ms m32-n32-k32 7 0.073ms( 0.00%) 0.010ms 0.013ms 0.009ms
- Unifiedメモリを用いた場合
- cublasSgemm_v2 : [3998602916 ns; 3.998603e+00 s;100.00%] params count sum avg max min m16384-n16384-k16384 7 3477.529ms( 86.97%) 496.790ms 498.403ms 496.514ms m8192-n8192-k8192 7 438.032ms( 10.95%) 62.576ms 65.861ms 58.048ms m4096-n4096-k4096 7 74.214ms( 1.86%) 10.602ms 12.583ms 9.095ms m2048-n2048-k2048 7 6.807ms( 0.17%) 0.972ms 0.979ms 0.969ms m1024-n1024-k1024 7 1.345ms( 0.03%) 0.192ms 0.203ms 0.190ms m512-n512-k512 7 0.306ms( 0.01%) 0.044ms 0.052ms 0.042ms m256-n256-k256 7 0.120ms( 0.00%) 0.017ms 0.020ms 0.017ms m128-n128-k128 7 0.093ms( 0.00%) 0.013ms 0.015ms 0.013ms m64-n64-k64 7 0.092ms( 0.00%) 0.013ms 0.025ms 0.009ms m32-n32-k32 7 0.065ms( 0.00%) 0.009ms 0.011ms 0.009ms
どの大きさの行列でもほぼ同程度の実行時間となり、1の評価のN=32のときのように10倍の差があるということもないことが分かりました。
Device側のメモリ上にある分にはアクセス速度は同程度なようです。
まとめ
今回の実験では、Unifiedメモリを用いた場合ではcudaMemcpyと比較してHtoDは遅いが、一旦Device側にデータを送りさえすればDevice側でのアクセス速度はほぼ同じであることが確認できました。 Unifiedメモリを用いた場合はHtoDが遅いですが、問題サイズによってはここの遅さは無視できそうです。
終わりに
自分でcudaMemcpyを書くことに苦がない場合はUnifiedメモリは使わないほうが良さそうですね。
余談ですが、Unifiedメモリは私が大学受験の時期に発表されたもので、大学に入って久々にCUDAを触ったら明示的なメモリコピーが不要となっていて技術の進歩に驚いた覚えがあります。
評価環境
- GPU : NVIDIA RTX 3080
- CPU : Intel(R) Xeon(R) E-2136 CPU @ 3.30GHz
- RAM : DDR4 2666MT/s
- CUDA : 11.3