Tensorコアを使ってみた
アルバイトの大友です。
TensorコアのWMMA APIを使っている人があまりいなかったため、6月中はインターンとして、7月からはアルバイトとしてその使い方や性能を調べていました。
この記事はその成果をまとめたものです。
Tensorコアを使うことでFP16のSIMD計算(f16x2)に比べ密行列積を5倍程度高速化できました。
Tensorコアとは
NVIDIA Voltaアーキテクチャから採用されたTensorコアは2つののFP16行列の積を1サイクルで計算し、その累積和をFP16/FP32で取ることができる計算ユニットです。
cuBLAS, cuDNNなどのライブラリではCUDA 9からTensorコアを利用できます。
WMMA APIを用いた行列積計算
CUDA 9ではWMMA (Warp Matrix Multiply Accumulate) と呼ばれるTensorコアを使用してGEMM計算を行うためのC++ APIが用意されています。
このAPIではなど決められた大きさの行列をfragmentと呼ばれる構造体にスレッドごとに分割し、1ワープ(32スレッド)が協調してその行列積を計算します。
行列計算の流れ
行列に対しを計算する流れは次のようになります。
- 各スレッドがメモリから,それぞれの一部をfragmentとして読み込む
- 各スレッドのfragmentを用いて行列積を計算 (計算結果は同じくfragmentとして各スレッドが一部ずつ保持)
- 各スレッドがCのfragmentをメモリに書き込む
WMMA APIを使用したプログラム
の2行列の積を計算するCUDAのカーネル関数は次のように書けます。(CUDA 9.2.148)
このAPIでは1ワープでの行列の積を計算するため、カーネル関数は次のように呼び出します。
nvcuda::wmma::fragment構造体
各スレッドが保持するfragmentは
と定義されており、それぞれのテンプレート引数は次のような役割を担っています。
Use
: GEMM計算 のどの行列のfragmentか- の場合
nvcuda::wmma::matrix_a
- の場合
nvcuda::wmma::matrix_b
- の場合
nvcuda::wmma::accumulator
- の場合
m
,n
,k
: Tensorコアで計算する行列積の行列の大きさ
ただし、(m
,n
,k
)は (16, 16, 16), (32, 8, 16), (8, 32, 16)のいずれか- :
- :
- :
T
: fragmentの型- : half
- : half / float
Layout
: 列優先か行優先か- 列優先 :
nvcuda::wmma::col_major
- 行優先 :
nvcuda::wmma::row_major
- 列優先 :
メンバ変数
x
: fragmentの要素配列num_elements
: fragmentの要素数
nvcuda::wmma::fill_fragment関数
nvcuda::wmma::fragment a
の全要素にv
を代入
nvcuda::wmma::load_matrix_sync関数
fragmentをメモリから読み込む
引数
a
: 読み込み先fragmentmptr
: 読み込み元ポインタldm
: 行列全体のLeading dimensionlayout
: 列優先の場合はnvcuda::wmma::mem_col_major
, 行優先の場合はnvcuda::wmma::mem_row_major
制約
mptr
が128-bit境界である必要あり (Alignment制約)ldm
が16 bytesの倍数である必要あり (halfでは8, floatでは4) (Leading dimension制約)
nvcuda::wmma::store_matrix_sync関数
fragmentをメモリに書き出す
引数
mptr
: 書き出し先ポインタa
: 書き出し元fragment (nvcuda::wmma::accumulator
のみ)ldm
: 書き出し先行列のLeading dimensionlayout
: 列優先の場合はnvcuda::wmma::mem_col_major
, 行優先の場合はnvcuda::wmma::mem_row_major
制約
nvcuda::wmma::load_matrix_sync
と同様の制約と未定義動作あり
nvcuda::wmma::mma_sync関数
Tensorコアを用いたGEMM計算
引数
d, a, b, c
: GEMM計算 の各fragmentsatf
: fragmentの要素が+-Infinity, NaNとなった場合に有限値に修正するか否か
任意の大きさの行列積計算
WMMA APIでは決められた大きさの行列積しか計算できませんが、行列積を分解して考えることで任意の大きさの行列積を計算することができます。
行列の積を計算する流れは次のようになります。
- 行列を行列のブロックに分割する。(端数部分は0埋め)
- 上図ではと計算できる。
このようにと計算することができる。
はの2行列の積のため、Tensorコアを用いて計算する。 - 2をのすべてのブロックに対して行う。
WMMA APIを使用するにあたって
上述したとおり、nvcuda::wmma::load_matrix_sync
関数とnvcuda::wmma::store_matrix_sync
関数にはメモリのAlignment制約とLeading dimension制約があり、
Globalメモリにある任意の大きさの行列のGEMM計算を行うにはこの制約に対応しなければなりません。
そこでSharedメモリを用いることで対応します。
- fragmentとして読み込むGlobalメモリの領域をSharedメモリにコピー
- コピーしたSharedメモリから
nvcuda::wmma::load_matrix_sync
関数でfragmentに読み込み nvcuda::wmma::mma_sync
関数でGEMM計算- 計算結果のfragmentを
nvcuda::wmma::store_matrix_sync
関数でSharedメモリに書き出し - 書き出したSharedメモリからGlobalメモリに書き出し
注意点
SharedメモリであればAlignment制約が満たされるわけではないので、必要ならば__align__([n byte])
で境界を指定しなければならない。
性能調査
実験方法
- 行列に対しを計算
- Tensorコアを使用した場合としなかった場合(f16x2を用いた場合)で計算速度を比較
- それぞれ5回計算を行う
- 実験コードはtensorcore/matmul_evalにあります
実験環境
- CPU : Intel Core i9-7900X
- GPU : NVIDIA Titan V
- RAM : 64GB
- OS : Ubuntu 16.04
実験結果
Tensorコアを使用した場合、使用しなかった場合に比べてで5倍程度高速化されました。
nvcuda::wmma::fragment構造体の調査
行列がどのようにfragmentとしてワープ内で保持されているのかをprintfですべて標準出力して調査しました。
nvcuda::wmma::matrix_a, nvcuda::wmma::matrix_bの場合
行列を(m, n, k) = (16, 16, 16)
,nvcuda::wmma::col_major
なnvcuda::wmma::matrix_a
,nvcuda::wmma::matrix_b
それぞれのfragmentにloadする場合を考えます。
をのブロックに分割し
と表すとthreadIdx.x
の
nvcuda::wmma::matrix_a
のfragmentはnvcuda::wmma::matrix_b
のfragmentは
で表される行列の行目となります。
これを可視化すると
となります。
nvcuda::wmma::accumulatorの場合
行列は(m, n, k) = (16, 16, 16)
,nvcuda::wmma::col_major
なnvcuda::wmma::accumulator
のfragmentでは
をのブロックに分割し
と表すとthreadIdx.x
では
で表される行列の行目となります。
これを可視化すると
となります。
行優先の場合
Globalメモリに行列が行優先で置かれている場合、単純にfragmentに読み込む際に転置して読み込んでいるわけではありません。
行優先か列優先かでfragmentの中身が異なるため、nvcuda::wmma::mma_sync
関数に対応するPTX命令であるwmma.load
命令は
という構造となっており(3)、fragment a
,b
が行優先か列優先かを指定する必要があります。
に関しては行優先か列優先かを指定する必要はなく、実際列優先か行優先かでfragmentに差は見られませんでした。
nvcuda::wmma::load_matrix_syncの調査
Warp内の各スレッドでのnvcuda::wmma::fragment
構造体の中身がわかったので、nvcuda::wmma::load_matrix_sync
関数を使わずに自前でfragmentを読み込んだ場合と速度を比較しました。
- 行列を
nvcuda::wmma::matrix_a
として読み込むだけのカーネルを実行 - カーネル内で回Globalメモリから読み込みを実行
- nvprofでカーネルの実行時間を測定
- 実験コードはtensorcore/load_evalにあります
結果
関数 | 実行時間 |
load_matrix_sync 関数 |
91 us |
自作load関数 | 77670 us |
高速化を余り考えずに書いたと言え、自作load関数に比べてnvcuda::wmma::load_matrix_sync
関数が850倍程度高速という結果になりました。考察NVIDIA Visual Profilerで実行されたSASSコードを見たところ、nvcuda::wmma::load_matrix_sync
関数でも汎用的なメモリ読み込み命令であるLDG命令が使われているようでした。
読み込みアドレスの計算と実際の読み込み命令の実行順などが工夫されているのかもしれません。
まとめ
TensorコアはWMMA APIを用いることで簡潔に利用することができました。
WMMA APIのload_matrix_sync
,store_matrix_sync
,mma_sync
関数はほとんど単純にPTXの命令に置き換えられるだけなためレイヤーは低く、使用の自由度は高いと考えられます。
性能面ではTensorコアを使用することでf16x2を使用した場合に比べFP16密行列積を高速に計算できることが確認できました。
謝辞
吉藤さんにはインターン及びアルバイトでCUDAやコードの書き方について指導していただきました。
ありがとうございました。
参考文献
- NVIDIA Developer Blog – Programming Tensor Cores in CUDA 9
- CUDA Toolkit Document – CUDA C Programming Guide (Warp matrix functions)
- CUDA Toolkit Document – Parallel Thread Execution ISA (Warp Level Matrix Multiply-Accumulate Instructions)
- GitHub – parallel-forall/code-samples
- VOLTA AND TURING: ARCHITECTURE AND PERFORMANCE OPTIMIZATION – Akira Naruse, Developer Technology, 2018//14