Weitere ähnliche Inhalte Ähnlich wie Intel AVX2を使用したailia sdkの最適化 (20) Intel AVX2を使用したailia sdkの最適化5. ailia SDKの構成
Vulkan, Metal NEON AVX
Accelerator (Convolution, Pooling, Resize, Add, and more.)
Runtime Graph Optimization
API (C++, Python, C#, JNI)
ONNX (opset=10, 11) (supporting over 150 layers)
ailia SDK
ailia MODELS
6. ailia SDK の SIMD 高速化
※共に FP32 モデル / ※ CPU i7-11700 で測定
AVX2を使用することで6~約7倍の高速化を実現
3.7倍
6.2倍
4倍
6.9倍
7. SIMD の基礎
一命令で複数のデータを処理可能 ( MMX 16bit 加算命令 PADDW の例 )
0x0104
0x0203
0x0302
0x0401
0x0100
0x0200
0x0300
0x0400
0x0004
0x0003
0x0002
0x0001
= +
出力レジスタ (実行後) 出力レジスタ (実行前) 入力レジスタ
8. SIMD の利点
1. 少ないクロックでより多くのデータを処理可能
・AVX2 命令を使うと 32bit 整数加算を 1 clock で 24 個処理可能
2. 入力データに依存した分岐を潰せる(後述)
・実行時の分岐予測ミスを減らせる
3. プログラムから扱えるレジスタが増える
・x86 CPU は汎用レジスタが 16 個と少ない
・SIMD レジスタを活用すると中間データを置けるレジスタが倍に増える
9. 拡張命令 レジスタ幅 特徴
MMX 64 bit 整数命令のみ
SSE 128 bit 単精度浮動小数命令メイン
SSE2 〃 倍精度浮動小数命令と整数命令を追加
SSSE3 ~ SSE4.2 〃 命令の追加・拡張
AVX 256 bit 浮動小数命令のみ
AVX2 〃 整数命令を追加 / 同じタイミングで FMA 命令も追加
AVX512 512 bit 第12世代インテル Core CPU の Efficiency コアが非対応
x86 SIMD 命令の歴史
10. 拡張命令 レジスタ幅 特徴
MMX 64 bit 整数命令のみ
SSE 128 bit 単精度浮動小数命令メイン
SSE2 〃 倍精度浮動小数命令と整数命令を追加
SSSE3 ~ SSE4.2 〃 命令の追加・拡張
AVX 256 bit 浮動小数命令のみ
AVX2 〃 整数命令を追加 / 同じタイミングで FMA 命令も追加
AVX512 512 bit 第12世代インテル Core CPU の Efficiency コアが非対応
(第12世代でも使えないものがある)
x86 SIMD 命令の歴史
64bit CPU なら利用可能
Haswell (2013年発売) 以降の Core CPU なら利用可能
11. SIMD 命令を利用する方法
● intrinsic を使って C/C++ コードから直接利用
特別なコンパイラは不要
Visual C/C++ Compiler や GCC, LLVM で利用可能
VC2019 以降の場合、通常は intrinsic で十分な性能が出る
● アセンブリで関数を作ってリンク
intrinsic でコンパイラが生成する命令列が遅い場合の最終手段
12. Intrinsic コードサンプル
xorps xmm0, xmm0
movups xmm1 [esi]
cmpps xmm0, xmm1, 0x1
andps xmm0, xmm1
movups [edi], xmm0
// 0 で埋めたレジスタを作る
__m128 zero = _mm_setzero_ps();
// 入力をレジスタに読み込む
__m128 val = _mm_loadu_ps(src);
// 0 が入力よりも小さければ bit を立てる
__m128 mask = _mm_cmplt_ps(zero, val);
// mask と入力でビット演算で and を取る
val = _mm_and_ps(val, mask);
// 結果を dst に書き出す
_mm_storeu_ps(dst, val);
アセンブリ Intrinsics
13. 同等コードの pure C/C++ バージョン
void relu_4element(float *dst, const float *src) {
for (int i=0; i<4; ++i) {
// 入力が負なら 0 に潰し、正ならば素通し
if (src[i] < 0.0f) {
dst[i] = 0.0f;
} else {
dst[i] = src[i];
}
}
}
14. 同等コードの pure C/C++ バージョン
void relu_4element(float *dst, const float *src) {
for (int i=0; i<4; ++i) {
// 入力が負なら 0 に潰し、正ならば素通し
if (src[i] < 0.0f) {
dst[i] = 0.0f;
} else {
dst[i] = src[i];
}
}
}
入力データに依存したランダムな分岐処理
CPU は苦手
15. Intrinsics コードサンプル
xorps xmm0, xmm0
movups xmm1 [esi]
cmpps xmm0, xmm1, 0x1
andps xmm0, xmm1
movups [edi], xmm0
// 0 で埋めたレジスタを作る
__m128 zero = _mm_setzero_ps();
// 入力をレジスタに読み込む
__m128 val = _mm_loadu_ps(src);
// 0 が入力よりも小さければ bit を立てる
__m128 mask = _mm_cmplt_ps(zero, val);
// mask と入力でビット演算で and を取る
val = _mm_and_ps(val, mask);
// 結果を dst に書き出す
_mm_storeu_ps(dst, val);
アセンブリ Intrinsics
条件分岐無し
18. AVX2 対応 CPU 向け高速化テクニック
• FMA 命令の利用
• MASKMOV 命令を使った端数処理
• 近似式を用いた高級関数の SIMD 実装
19. FMA 命令の利用
● FMA = Fused Multiply Add
● D = A * B + C を 1 命令で処理
● AVX2 と同じ Haswell 以降で利用可能
● 行列積や Neural Network の Convolution で大量に出現
● FMA を使わないと(使わない場合に比べて) 20~10% 性能低下
20. MASKMOV 命令を使った端数処理
● SIMD レジスタ幅で割り切れない入力の場合に、端数部分で活用
● AVX 命令から追加
● MASKMOV の無い CPU の場合
SIMD レジスタの load/store に中間バッファが必要
● MASKMOV がある CPU の場合
MASKMOVで直接 SIMD レジスタに load/store 可能
25. MASKMOV サンプル
inline __m256i select_mask(int count) {
assert((0 <= count) || (count <= 8));
auto m0 = _mm256_set_epi32(7, 6, 5, 4, 3, 2, 1, 0);
auto ulim =
_mm256_broadcastd_epi32(
_mm_cvtsi32_si128(count)
);
return _mm256_cmpgt_epi32(ulim, m0);
}
void func( float *buf, int size ) {
int size8 = (size / 8) * 8;
for (int i=0; i<size8; i+=8) { // 本体部分
auto val = _mm256_loadu_ps( buf+i );
// val に対する何らかの SIMD 処理をここに記述
_mm256_storeu_ps( buf+i, val );
}
if (size8 < size ) { // 端数処理
auto mask = select_mask(size - size8);
auto val = _mm256_maskload_ps(buf+size8, mask);
// val に対する何らかの SIMD 処理をここに記述
_mm256_maskstore_ps(buf+size8, mask, val);
}
}
26. 近似式を使った高級関数の SIMD 実装
// log(x) = log((2^n) * z) = n*log(2) + log(z)
// log(z) ≒ 2 * (w + (w^3)/3 + (w^5)/5 + ..) : w = (z-1)/(z+1)
log2 = 0.6931471805599453f;
n = pick_exponent(x); // IEEE754 の指数部を取り出す
z = pick_fractional(x); // IEEE754 の指数部を 0 にする
w = (z-1.0f) / (z+1.0f);
ww = w*w;
r = (1.0f/7.0f) + (1.0f/9.0f)*ww;
r = (1.0f/5.0f) + r*ww;
r = (1.0f/3.0f) + r*ww;
r = (1.0f + r*ww);
r = r*w; // w + (w^3)/3 + (w^5)/5 + (w^7)/7 + (w^9)/9
return (n*log2 + r*2);
log() の分岐不要な近似式疑似コード。 exp() や erf() も同様の近似式実装が可能。