SlideShare a Scribd company logo
expの実装に見る
AVX-512とSVEの違い
2020/12/3
第9回HPC-Phys勉強会
光成滋生
• expの近似計算
• AVX-512による実装
• SVEによる実装
• レジスタリネーミング
• SVEのfexpaを使った近似計算
• 実装
• 逆数近似命令
目次
2 / 27
• 級数展開を使う
• 𝑒 𝑥 = 1 + 𝑥 +
𝑥2
2
+
𝑥3
6
+ ⋯
• 要請
• 級数展開における𝑥は小さいのが望ましい
• 2 𝑛(𝑛が整数)は高速に求められる
• 式変形
• 𝑒 𝑥
= 2 𝑥 log2 𝑒
• 𝑦 ≔ 𝑥 log2 𝑒 を整数部分𝑛と小数部分𝑎に分ける( 𝑎 ≤ 1/2)
• 𝑦 = 𝑛 + 𝑎
• 𝑒 𝑥 = 2 𝑦 = 2 𝑛+𝑎 = 2 𝑛2 𝑎
• 2 𝑎 = 𝑒 𝑎 log(2)
• 𝑏 ≔ 𝑎 log(2), 𝑏 ≤ log 2 /2 = 0.346
exp(float x);の近似計算(1/3)
3 / 27
• floatなら1e-6程度まで求まれば十分
• 5次の項まで計算
• 𝑒 𝑥
= 1 + 𝑥 +
𝑥2
2
+
𝑥3
6
+
𝑥4
24
+
𝑥5
120
• 係数補正
• 𝑓 𝑥 ≔ 1 + 𝑥 + 𝐵𝑥 + 𝐶𝑥2 + 𝐷𝑥3 + 𝐸𝑥4 + 𝐹𝑥5
• 𝑥 ∈ [−𝐿, 𝐿] where 𝐿 ≔ log(2)/2
• 𝐼 ≔ ‫׬‬−𝐿
𝐿
𝑓 𝑥 − 𝑒 𝑥 2 𝑑𝑥を最小化する(𝐵, 𝐶, 𝐷, 𝐸, 𝐹)を求める
• 1次の項(𝐴)を1に固定しているのは0次と共用したいから
• 誤差が概ね半分程度になる
• Sollyaのremezよりは誤差が小さい?
• L2ノルムとL1ノルムどちらがよいかアプリ依存?
exp(float x);の近似計算(2/3)
4 / 27
• まとめ
• 配列に対する一括処理
• テーブルを使わないためSIMD化しやすい
exp(float x);の近似計算(3/3)
input : x
x = x * log_2(e)
n = round(x) ; 四捨五入
a = x - n
b = a * log(2)
c = 1 + b(1 + b(B + b(C + b(D + E b))))
y = c * 2^n
output : y
void expv(float *dst, const float *src, size_t n) {
for (size_t i = 0; i < n; i++) dst[i] = exp(src[i]);
}
5 / 27
• Intelの512bit幅のSIMD命令セット
• 32個の512bit AVXレジスタ
• double x 8, float x 16, int32 x 16, int16 x 32, int8 x 64などの型
• 概ね3オペランド
• op(r1, r2, r3) ; r1 = r2 op r3
• d ; 32-bit int, ps ; float, pd ; double
• 積和演算FMA
• d = a * b + cが望ましいが4オペランドになるので
AVX-512
paddd zmm0, zmm1, zmm2 // zmm0 = zmm1 + zmm2 as 32-bit int
addps zmm0, zmm1, zmm2 // as float
addpd zmm0, zmm1, zmm2 // as double
vfmadd132ps r1, r2, r3 // r1 = r1 * r3 + r2
vfmadd213ps r1, r2, r3 // r1 = r2 * r1 + r3
vfmadd231ps r1, r2, r3 // r1 = r2 * r3 + r1
6 / 27
• C++でCPUのニーモニックを記述
• 実行時にそれに対応する機械語が生成されて実行する
• 例 : 「整数nを足す関数」を生成する関数
• genAdd(5); // 「5を足す関数」が実行時に生成される
• 生成された関数
• exp自体は静的な関数でよいがIntel oneDNNではパーツの一つ
として実行時生成されている
Xbyak/Xbyak_aarch64
struct Code : Xbyak::CodeGenerator {
void genAdd(int n) {
// rsiはLinuxでの関数の第一引数
lea(rax, ptr[rsi + n]);
ret();
} };
lea rax, [rsi + 5] // + 5は即値
ret
7 / 27
• round関数
• vrndscaleps(dst, src, round_ctl)
• round_ctl ; 2bitフラグ
• 00 ; nearest even ; 一番近い偶数丸め
• 01 ; equal or smaller ; 切り捨て
• 10 ; equal or larger ; 切り上げ
• 11 ; truncate ; 0方向に切り捨て
• vrndscaleps(dst, src, 0) ; dst = round(src);
• c = 1 + b(1 + b(B + b(C + b(D + E b))))
• FMAを5回適用
• 𝑦 = 𝑐 × 2 𝑛の部分
• AVX2までは整数𝑛をビットシフトしてfloatに変換して...
• AVX-512ではvscalefps(dst, r1, r2) // dst = r1 * 2^r2
AVX-512によるexp
8 / 27
• 準備
• 下記変数はzmmレジスタのエリアス名
• log2_e, log2, one, B, C, D, Eは事前に定数設定
• t1, t2は一時レジスタ, xが入出力(x = exp(x))
exp一つ分
genExpOne() {
vmulps(x, log2_e); // x *= log2_e
vrndscaleps(t1, x, 0); // t1 = round(x)
vsubps(x, t1); // x = x - t1 ; 小数部分a
vmulps(x, log2); // a * log2
vmovaps(t2, E);
vfmadd213ps(t2, x, D);
vfmadd213ps(t2, x, C);
vfmadd213ps(t2, x, B);
vfmadd213ps(t2, x, one);
vfmadd213ps(t2, x, one); // t2 = 1+b(1+b(B+b(C+b(D+E b))))
vscalefps(x, t2, t1); // x = t2 * 2^t1
}
9 / 27
• ループnが16の倍数でないときの扱い
• マスクレジスタk1, ..., k7
• zmmレジスタのkに対応するbitが1なら処理, 0なら非処理
• T_zを指定すると非処理の部分に0が入る
• vmovups(zm0|k1|T_z, ptr[src]);
• readしない部分はメモリアクセスしない(アクセス違反しない)
• マスクレジスタを指定しないときに比べてやや遅い
• 16の倍数のときは指定しない方がよい
端数処理(1/2)
k1 = 0b00..01111111 (8bitの1)のとき
src | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| ...
|← readしない →
zm0 | x0| x1| x2| x3| x4| x5| x6| x7| 0| | 0| 0|...
10 / 27
• コア部分
端数処理(2/2)
Label lp = L(); // メインループ
vmovups(zm0, ptr[src]); // zm0 = *src
add(src, 64); // src += 64
genExpOne() // zm0 = exp(zm0)
vmovups(ptr[dst], zm0); // *dst = zm0
add(dst, 64); // dst += 64
sub(n, 16); // n -= 16
jnz(lp); // if (n != 0) goto lp
L(mod16); // 端数処理
and_(ecx, 15); // n &= 15
jz(exit);
mov(eax, 1);
shl(eax, cl); // eax = 1 << n
sub(eax, 1); // eax = (1 << n) - 1
kmovd(k1, eax); // k1 = nビットの1
vmovups(zm0|k1|T_z, ptr[src]); // 残り読み
genExpOne() // zm0 = exp(zm0)
vmovups(ptr[dst]|k1, zm0|k1); // 残り書き
L(exit);
11 / 27
• float x[16384];に対するstd::exp(float)との比較(clk)
• Xeon Platinum 8280 2.7GHz, g++ 9.3.0 Ubuntu 20.04.1 LTS
• genOneExpをループアンロール
• fmath::exp 8.7clk→7.2clk
• https://github.com/herumi/fmath/blob/master/fmath2.hpp
ベンチマーク
std::exp fmath::exp
140.1 8.7
genExpTwo() {
vmulps(x0, log2_e);
vmulps(x3, log2_e);
vrndscaleps(t1, x0, 0);
vrndscaleps(t4, x1, 0);
vsubps(x0, t1);
vsubps(x3, t4);
...
12 / 27
• Armの可変長SIMD命令セット
• 128~1024(?)bitレジスタ
• double x 8, float x 16, int32 x 16, int16 x 32, int8 x 64などの型
• 概ね3オペランド
• A64FX(富岳のCPU)では32個の512bitレジスタz0, ..., z31
• movprfx(dstをsrcとして利用)
• predは述語レジスタ
• マスクレジスタ相当
• AVX-512と異なり常に指定
SVE
fadd z0.s, z1.s, z2.s // as float
add z0.b, z1.b, z2.b // as int8
movprfx(dst, pred, r1);
fmadd(dst, pred, r2, r3); // dst = r1 * r2 + r3
13 / 27
• SVEレジスタの各要素を処理する(1)か否(0)かを指定
• 例 ld1w(z.s, p/T_z, ptr(src));
• z = *src; // float x 16個読み込み
• i番目の各要素(i = 0, ..., 15)について
• 述語レジスタp[i] = 1ならz[i] = src1[i]
• p[i] = 0ならT_z(zero)を指定しているのでz[i] = 0
• T_zを指定しなければp[i]の値を変更しない
述語レジスタ
src x0 x1 x2 x3...
z.s x0 0 x2 x3...
p 1 0 1 1
14 / 27
• 「x4 + i < n」が成り立つ添え字までp[i] = 1にする
• 例 x4 + 16 <= nならi = 0, ..., 15についてp[i] = 1
• 全てのデータが有効
• x4 + 3 = nならi ≦ 2についてp[i] = 1, その他p[i] = 0
• p[i] = 0の部分はデータを読まない・書かない
• 読み書き属性が無い領域でも大丈夫
whilelt(p.s, x4, n);
i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
p 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
p 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0
15 / 27
• メイン部分
• x4 + 16 ≦ nである限りp0[i] = 1 for i = 0, ..., 15
• ループの最終ではp0[i] = 1 for i <(n % 16), p0[i] = 0(otherwise)
• メリット
• SVEが256bitや1024bitでも同じコードで動く
• SVEはScalable Vector Extensionの略
• デメリット
• あまり速くない ; 結局AVX-512のように分けた方がよい
ループの終わり処理
Label lp = L();
ld1w(z0.s, p0/T_z, ptr(src1, x4, LSL, 2));// z0 = src1[x4 << 2]
...
incd(x4); // x4 += 16
L(cond);
whilelt(p0.s, x4, n); // while (x4 < n)なら
b_first(lp); // lpラベルにジャンプ
16 / 27
• AVX-512とほぼ同じ
• 述語レジスタpはall 1に設定
• round関数はfrintn
• fscale(x, p, n)のnがfloatではなくintなのでfcvtzsでintに変換
sveによるgenExpOne
fmul(tz0, tz0, log2_e);
frintn(tz2, p, tz0); // round : float -> float
fcvtzs(tz1, p, tz2); // float -> int
fsub(tz2, tz0, tz2);
fmul(tz2, tz2, log2);
movprfx(tz0, p, E);
fmad(tz0, p, tz2, D); // tz0 = tz2 * E + D
fmad(tz0, p, tz2, C);
fmad(tz0, p, tz2, B);
fmad(tz0, p, tz2, one);
fmad(tz0, p, tz2, one);
fscale(tz0, p, tz1); // tz0 *= 2^tz1
17 / 27
• frintnなどの命令のあとの依存関係
• レジスタ名を入れ換えると2倍速くなるケース(74→32nsec)
• https://github.com/herumi/misc/commit/0362d5647f693be66
da841eea7ca333d0f5b5329
レジスタリネーミングが苦手?
18 / 27
• 利用レジスタを増やしてループ展開
ループアンロール
// n = 1 or 2 or 3
for (int i = 0;i<n;i++) fmul(t[0+i*3], t[0+i*3], log2_e);
for (int i = 0;i<n;i++) frintn(t[2+i*3], p, t[0+i*3]);
for (int i = 0;i<n;i++) fcvtzs(t[1+i*3], p, t[2+i*3]);
for (int i = 0;i<n;i++) fsub(t[2+i*3], t[0+i*3], t[2+i*3]);
for (int i = 0;i<n;i++) fmul(t[2+i*3], t[2+i*3], log2);
for (int i = 0;i<n;i++) {
movprfx(t[0+i*3], p, E);
fmad(t[0+i*3], p, t[2+i*3], D);
}
for (int i = 0;i<n;i++) fmad(t[0+i*3], p, t[2+i*3], C);
for (int i = 0;i<n;i++) fmad(t[0+i*3], p, t[2+i*3], B);
for (int i = 0;i<n;i++) fmad(t[0+i*3], p, t[2+i*3], one);
for (int i = 0;i<n;i++) fmad(t[0+i*3], p, t[2+i*3], one);
for (int i = 0;i<n;i++) fscale(t[0+i*3], p, t[1+i*3]);
19 / 27
• float x[16384];に対するstd::exp(float)との比較(nsec)
• FX700
• A64FXはXeonに比べてレイテンシが大きい
• https://github.com/fujitsu/A64FX
• frintn, fmul, fadd; 9clk
• fdiv, 98clk
• ループアンロールの効果が大きい
ベンチマーク
std::exp fmath::exp, N=1 N=2 N=3
217.3 19.0 11.8 8.4
20 / 27
• pow(2, i/64) (i=0,...,63)のテーブル引き命令
• floatのbit表現
• u=|s:1|e:8|f:23|, 𝑓 = −1 𝑠
2 𝑒−127
(1 +
𝑓
224), e:指数部, f:仮数部
• tbl[i] := 「pow(2, i/64)」の下位23bit
• 𝑥 = 2
𝑖
64 for 𝑖 = 0, … , 63は1 ≤ 𝑥 < 2なので常にx.e=127
• fexp(x)の挙動
SVEのfexpa
x.s x.e x.f
v=6bity=8bit
tbl[]
tbl[v]y=8bit0
21 / 27
• 2ベキの近似なのでそれにもっていく
• exp 𝑥 = 2 𝑥 log2 𝑒
= 2 𝑦
where 𝑦 ≔ 𝑥 log2 𝑒
• 𝑦 = 𝑛 + 𝑎 where 𝑛 ≔ floor(𝑦), 𝑎 ≔ 𝑛 − 𝑦, 0 ≤ 𝑎 < 1
• fexpaの添え字vが2ベキになるには[1, 2)にある必要性
• 𝑏 ≔ 1 + 𝑎とすると1 ≤ 𝑏 < 2, 𝑦 = 𝑛 − 1 + 𝑏
• 𝑏.f上位6bit vを取り出して使う c = fexpa(b.f >> 17)
• 𝑏 = 𝑏. f/224=v /26 + w/224, 𝑧 ≔ w/224
• 2 𝑏 =fexpa(v)⋅ 2 𝑧, 2 𝑧 = 𝑒log 2 𝑧 = 1 + log 2 𝑧 + log 2 𝑧 2/2
• 差分が小さいので2次の項まででよい
• https://github.com/herumi/misc/blob/master/sve/fmath-sve.hpp
fexpaの使い方
𝑏.f
v=6bit w=17bit
22 / 27
• 主要コード概要
fexpaによるexp
fmul(t0, t0, para.log2_e);
frintm(t1, p, t0); // floor : float -> float
fcvtzs(t2, p, t1); // n = float -> int
fsub(t1, t0, t1); // a
fadd(t1, t1, one); // b = 1 + a
lsr(t3, t1, 17); // v = b >> 17
fexpa(t3, t3); // c = fexpa(v)
fscale(t3, p, t2); // t3 *= 2^n
and_(t2.d, t1.d, not_mask17.d);
fsub(t2, t1, t2); // z
movprfx(t0, p, coeff2);
fmad(t0, p, t2, coeff1);
fmad(t0, p, t2, one); // 1 + log2 z + (log2)^2/2 z^2
fmul(t0, t3, t0);
23 / 27
• float x[16384];に対するstd::exp(float)との比較(nsec)
• ()内はfxpaを使わないバージョン
• 気持ち速いかも?
• 定数レジスタの個数 7 vs 5
• 中間レジスタの個数 3 vs 4
• 雑感
• 最初の方法に比べてレジスタの依存関係が複雑になる
• 定数レジスタが少ないのはよいがそこまでのメリットが
• もう少し精度があれば
• うまく速くなるレジスタ割り当てに試行錯誤(全数探索?)
ベンチマーク
std::exp N=1 N=2 N=3
217.3 18.2
(19.0)
11.3
(11.8)
8.4
(8.4)
24 / 27
• fdivの代わりに逆数近似命令(rcp)のあと補正する
• AVX-512
• SVE ; frecps(dst, src1, src2) ; // dst = 2 - src1 * src2
• 2回補正するとfloatに近い精度
逆数近似計算
t = rcp(x)
1/x = 2 * t - x t^2
// x : input/output
// t : temporary
// two : 2.0f
vrcp14ps(t, x);
vfnmadd213ps(x, t, two); // x = -xt + 2
vmulps(x, x, t);
25 / 27
• frintmあるいはaddの後のz0の逆数処理
• https://github.com/herumi/misc/blob/master/sve/inv.cpp
• ○で囲った部分が変?
• レジスタリネーミング?
逆数近似計算
// input : z0 (compute it with z0, z1, z2)
1. fdivr(z0.s, p0, one.s);
2. frecpe(z1.s, z0.s); frecps(z2.s, z0.s, z1.s);
fmul(z0.s, z1.s, z2.s);
3. frecpe(z1.s, z0.s); frecps(z3.s, z0.s, z1.s);
fmul(z0.s, z1.s, z3.s);
4. frecpe(z1.s, z0.s); frecps(z2.s, z0.s, z1.s);
fmul(z1.s, z1.s, z2.s);
frecps(z2.s, z0.s, z1.s); fmul(z0.s, z1.s, z2.s);
5. frecpe(z1.s, z0.s); frecps(z3.s, z0.s, z1.s);
fmul(z1.s, z1.s, z3.s);
frecps(z3.s, z0.s, z1.s); fmul(z0.s, z1.s, z3.s);
clk
frintm add
100 100
33 7.9
10.9 7.9
51 11.0
11.2 11.0
26 / 27
• AVX-512とSVE(A64FX)
• どちらも512-bitレジスタx32
• SVEの方がレイテンシが大きい
• ループアンロール重要
• レジスタリネーミングに気をつける場合がある?
まとめ
27 / 27

More Related Content

PDF
Halide による画像処理プログラミング入門
PDF
Intro to SVE 富岳のA64FXを触ってみた
PDF
不揮発メモリ(NVDIMM)とLinuxの対応動向について
PPTX
AVX-512(フォーマット)詳解
ODP
Format string Attack
PDF
CXL_説明_公開用.pdf
PDF
Intel AVX-512/富岳SVE用SIMDコード生成ライブラリsimdgen
PPTX
高速な暗号実装のためにしてきたこと
Halide による画像処理プログラミング入門
Intro to SVE 富岳のA64FXを触ってみた
不揮発メモリ(NVDIMM)とLinuxの対応動向について
AVX-512(フォーマット)詳解
Format string Attack
CXL_説明_公開用.pdf
Intel AVX-512/富岳SVE用SIMDコード生成ライブラリsimdgen
高速な暗号実装のためにしてきたこと

What's hot (20)

PDF
組み込み関数(intrinsic)によるSIMD入門
PDF
PPL 2022 招待講演: 静的型つき函数型組版処理システムSATySFiの紹介
PDF
フラグを愛でる
PDF
0章 Linuxカーネルを読む前に最低限知っておくべきこと
PPTX
コンテナネットワーキング(CNI)最前線
PDF
C++ マルチスレッドプログラミング
PDF
MySQL 5.7にやられないためにおぼえておいてほしいこと
PPTX
ディープラーニングをAWS LambdaとStep Functionで自動化する
PDF
Scala警察のすすめ
PDF
プログラミングコンテストでのデータ構造 2 ~平衡二分探索木編~
PDF
TCP/IPプロトコルスタック自作入門
PPTX
冬のLock free祭り safe
PDF
SAT/SMTソルバの仕組み
PDF
ARM CPUにおけるSIMDを用いた高速計算入門
PDF
中3女子でもわかる constexpr
PDF
PostgreSQLのトラブルシューティング@第5回中国地方DB勉強会
PDF
Hacking QNX
PPTX
[CEDEC2017] LINEゲームのセキュリティ診断手法
PDF
HalideでつくるDomain Specific Architectureの世界
PDF
基本に戻ってInnoDBの話をします
組み込み関数(intrinsic)によるSIMD入門
PPL 2022 招待講演: 静的型つき函数型組版処理システムSATySFiの紹介
フラグを愛でる
0章 Linuxカーネルを読む前に最低限知っておくべきこと
コンテナネットワーキング(CNI)最前線
C++ マルチスレッドプログラミング
MySQL 5.7にやられないためにおぼえておいてほしいこと
ディープラーニングをAWS LambdaとStep Functionで自動化する
Scala警察のすすめ
プログラミングコンテストでのデータ構造 2 ~平衡二分探索木編~
TCP/IPプロトコルスタック自作入門
冬のLock free祭り safe
SAT/SMTソルバの仕組み
ARM CPUにおけるSIMDを用いた高速計算入門
中3女子でもわかる constexpr
PostgreSQLのトラブルシューティング@第5回中国地方DB勉強会
Hacking QNX
[CEDEC2017] LINEゲームのセキュリティ診断手法
HalideでつくるDomain Specific Architectureの世界
基本に戻ってInnoDBの話をします
Ad

Similar to HPC Phys-20201203 (20)

PDF
optimal Ate pairing
PDF
From IA-32 to avx-512
PDF
LLVM最適化のこつ
PDF
Haswellサーベイと有限体クラスの紹介
PDF
Intel AVX2を使用したailia sdkの最適化
PDF
llvm入門
PDF
OCamlのアセンブラを読む話
PDF
高速な倍精度指数関数expの実装
PDF
x64 のスカラー,SIMD 演算性能を測ってみた @ C++ MIX #10
PPTX
PBL1-v1-007j.pptx
PDF
x64 のスカラー,SIMD 演算性能を測ってみた v0.1 @ C++ MIX #10
PDF
条件分岐とcmovとmaxps
PDF
El text.tokuron a(2019).watanabe190613
PPTX
Prosym2012
PDF
V6 unix in okinawa
PDF
LLVMで遊ぶ(整数圧縮とか、x86向けの自動ベクトル化とか)
PDF
あまぁいRcpp生活
PPTX
Fftw誰得ガイド
PDF
機械学習とこれを支える並列計算 : 並列計算の現状と産業応用について
optimal Ate pairing
From IA-32 to avx-512
LLVM最適化のこつ
Haswellサーベイと有限体クラスの紹介
Intel AVX2を使用したailia sdkの最適化
llvm入門
OCamlのアセンブラを読む話
高速な倍精度指数関数expの実装
x64 のスカラー,SIMD 演算性能を測ってみた @ C++ MIX #10
PBL1-v1-007j.pptx
x64 のスカラー,SIMD 演算性能を測ってみた v0.1 @ C++ MIX #10
条件分岐とcmovとmaxps
El text.tokuron a(2019).watanabe190613
Prosym2012
V6 unix in okinawa
LLVMで遊ぶ(整数圧縮とか、x86向けの自動ベクトル化とか)
あまぁいRcpp生活
Fftw誰得ガイド
機械学習とこれを支える並列計算 : 並列計算の現状と産業応用について
Ad

More from MITSUNARI Shigeo (20)

PDF
暗号技術の実装と数学
PDF
範囲証明つき準同型暗号とその対話的プロトコル
PDF
暗認本読書会13 advanced
PDF
暗認本読書会12
PDF
暗認本読書会11
PDF
暗認本読書会10
PDF
暗認本読書会9
PDF
暗認本読書会8
PDF
暗認本読書会7
PDF
暗認本読書会6
PDF
暗認本読書会5
PDF
暗認本読書会4
PDF
深層学習フレームワークにおけるIntel CPU/富岳向け最適化法
PDF
私とOSSの25年
PDF
WebAssembly向け多倍長演算の実装
PDF
Lifted-ElGamal暗号を用いた任意関数演算の二者間秘密計算プロトコルのmaliciousモデルにおける効率化
PDF
楕円曲線と暗号
PDF
BLS署名の実装とその応用
PDF
LazyFP vulnerabilityの紹介
PDF
ゆるバグ
暗号技術の実装と数学
範囲証明つき準同型暗号とその対話的プロトコル
暗認本読書会13 advanced
暗認本読書会12
暗認本読書会11
暗認本読書会10
暗認本読書会9
暗認本読書会8
暗認本読書会7
暗認本読書会6
暗認本読書会5
暗認本読書会4
深層学習フレームワークにおけるIntel CPU/富岳向け最適化法
私とOSSの25年
WebAssembly向け多倍長演算の実装
Lifted-ElGamal暗号を用いた任意関数演算の二者間秘密計算プロトコルのmaliciousモデルにおける効率化
楕円曲線と暗号
BLS署名の実装とその応用
LazyFP vulnerabilityの紹介
ゆるバグ

HPC Phys-20201203

  • 2. • expの近似計算 • AVX-512による実装 • SVEによる実装 • レジスタリネーミング • SVEのfexpaを使った近似計算 • 実装 • 逆数近似命令 目次 2 / 27
  • 3. • 級数展開を使う • 𝑒 𝑥 = 1 + 𝑥 + 𝑥2 2 + 𝑥3 6 + ⋯ • 要請 • 級数展開における𝑥は小さいのが望ましい • 2 𝑛(𝑛が整数)は高速に求められる • 式変形 • 𝑒 𝑥 = 2 𝑥 log2 𝑒 • 𝑦 ≔ 𝑥 log2 𝑒 を整数部分𝑛と小数部分𝑎に分ける( 𝑎 ≤ 1/2) • 𝑦 = 𝑛 + 𝑎 • 𝑒 𝑥 = 2 𝑦 = 2 𝑛+𝑎 = 2 𝑛2 𝑎 • 2 𝑎 = 𝑒 𝑎 log(2) • 𝑏 ≔ 𝑎 log(2), 𝑏 ≤ log 2 /2 = 0.346 exp(float x);の近似計算(1/3) 3 / 27
  • 4. • floatなら1e-6程度まで求まれば十分 • 5次の項まで計算 • 𝑒 𝑥 = 1 + 𝑥 + 𝑥2 2 + 𝑥3 6 + 𝑥4 24 + 𝑥5 120 • 係数補正 • 𝑓 𝑥 ≔ 1 + 𝑥 + 𝐵𝑥 + 𝐶𝑥2 + 𝐷𝑥3 + 𝐸𝑥4 + 𝐹𝑥5 • 𝑥 ∈ [−𝐿, 𝐿] where 𝐿 ≔ log(2)/2 • 𝐼 ≔ ‫׬‬−𝐿 𝐿 𝑓 𝑥 − 𝑒 𝑥 2 𝑑𝑥を最小化する(𝐵, 𝐶, 𝐷, 𝐸, 𝐹)を求める • 1次の項(𝐴)を1に固定しているのは0次と共用したいから • 誤差が概ね半分程度になる • Sollyaのremezよりは誤差が小さい? • L2ノルムとL1ノルムどちらがよいかアプリ依存? exp(float x);の近似計算(2/3) 4 / 27
  • 5. • まとめ • 配列に対する一括処理 • テーブルを使わないためSIMD化しやすい exp(float x);の近似計算(3/3) input : x x = x * log_2(e) n = round(x) ; 四捨五入 a = x - n b = a * log(2) c = 1 + b(1 + b(B + b(C + b(D + E b)))) y = c * 2^n output : y void expv(float *dst, const float *src, size_t n) { for (size_t i = 0; i < n; i++) dst[i] = exp(src[i]); } 5 / 27
  • 6. • Intelの512bit幅のSIMD命令セット • 32個の512bit AVXレジスタ • double x 8, float x 16, int32 x 16, int16 x 32, int8 x 64などの型 • 概ね3オペランド • op(r1, r2, r3) ; r1 = r2 op r3 • d ; 32-bit int, ps ; float, pd ; double • 積和演算FMA • d = a * b + cが望ましいが4オペランドになるので AVX-512 paddd zmm0, zmm1, zmm2 // zmm0 = zmm1 + zmm2 as 32-bit int addps zmm0, zmm1, zmm2 // as float addpd zmm0, zmm1, zmm2 // as double vfmadd132ps r1, r2, r3 // r1 = r1 * r3 + r2 vfmadd213ps r1, r2, r3 // r1 = r2 * r1 + r3 vfmadd231ps r1, r2, r3 // r1 = r2 * r3 + r1 6 / 27
  • 7. • C++でCPUのニーモニックを記述 • 実行時にそれに対応する機械語が生成されて実行する • 例 : 「整数nを足す関数」を生成する関数 • genAdd(5); // 「5を足す関数」が実行時に生成される • 生成された関数 • exp自体は静的な関数でよいがIntel oneDNNではパーツの一つ として実行時生成されている Xbyak/Xbyak_aarch64 struct Code : Xbyak::CodeGenerator { void genAdd(int n) { // rsiはLinuxでの関数の第一引数 lea(rax, ptr[rsi + n]); ret(); } }; lea rax, [rsi + 5] // + 5は即値 ret 7 / 27
  • 8. • round関数 • vrndscaleps(dst, src, round_ctl) • round_ctl ; 2bitフラグ • 00 ; nearest even ; 一番近い偶数丸め • 01 ; equal or smaller ; 切り捨て • 10 ; equal or larger ; 切り上げ • 11 ; truncate ; 0方向に切り捨て • vrndscaleps(dst, src, 0) ; dst = round(src); • c = 1 + b(1 + b(B + b(C + b(D + E b)))) • FMAを5回適用 • 𝑦 = 𝑐 × 2 𝑛の部分 • AVX2までは整数𝑛をビットシフトしてfloatに変換して... • AVX-512ではvscalefps(dst, r1, r2) // dst = r1 * 2^r2 AVX-512によるexp 8 / 27
  • 9. • 準備 • 下記変数はzmmレジスタのエリアス名 • log2_e, log2, one, B, C, D, Eは事前に定数設定 • t1, t2は一時レジスタ, xが入出力(x = exp(x)) exp一つ分 genExpOne() { vmulps(x, log2_e); // x *= log2_e vrndscaleps(t1, x, 0); // t1 = round(x) vsubps(x, t1); // x = x - t1 ; 小数部分a vmulps(x, log2); // a * log2 vmovaps(t2, E); vfmadd213ps(t2, x, D); vfmadd213ps(t2, x, C); vfmadd213ps(t2, x, B); vfmadd213ps(t2, x, one); vfmadd213ps(t2, x, one); // t2 = 1+b(1+b(B+b(C+b(D+E b)))) vscalefps(x, t2, t1); // x = t2 * 2^t1 } 9 / 27
  • 10. • ループnが16の倍数でないときの扱い • マスクレジスタk1, ..., k7 • zmmレジスタのkに対応するbitが1なら処理, 0なら非処理 • T_zを指定すると非処理の部分に0が入る • vmovups(zm0|k1|T_z, ptr[src]); • readしない部分はメモリアクセスしない(アクセス違反しない) • マスクレジスタを指定しないときに比べてやや遅い • 16の倍数のときは指定しない方がよい 端数処理(1/2) k1 = 0b00..01111111 (8bitの1)のとき src | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| ... |← readしない → zm0 | x0| x1| x2| x3| x4| x5| x6| x7| 0| | 0| 0|... 10 / 27
  • 11. • コア部分 端数処理(2/2) Label lp = L(); // メインループ vmovups(zm0, ptr[src]); // zm0 = *src add(src, 64); // src += 64 genExpOne() // zm0 = exp(zm0) vmovups(ptr[dst], zm0); // *dst = zm0 add(dst, 64); // dst += 64 sub(n, 16); // n -= 16 jnz(lp); // if (n != 0) goto lp L(mod16); // 端数処理 and_(ecx, 15); // n &= 15 jz(exit); mov(eax, 1); shl(eax, cl); // eax = 1 << n sub(eax, 1); // eax = (1 << n) - 1 kmovd(k1, eax); // k1 = nビットの1 vmovups(zm0|k1|T_z, ptr[src]); // 残り読み genExpOne() // zm0 = exp(zm0) vmovups(ptr[dst]|k1, zm0|k1); // 残り書き L(exit); 11 / 27
  • 12. • float x[16384];に対するstd::exp(float)との比較(clk) • Xeon Platinum 8280 2.7GHz, g++ 9.3.0 Ubuntu 20.04.1 LTS • genOneExpをループアンロール • fmath::exp 8.7clk→7.2clk • https://github.com/herumi/fmath/blob/master/fmath2.hpp ベンチマーク std::exp fmath::exp 140.1 8.7 genExpTwo() { vmulps(x0, log2_e); vmulps(x3, log2_e); vrndscaleps(t1, x0, 0); vrndscaleps(t4, x1, 0); vsubps(x0, t1); vsubps(x3, t4); ... 12 / 27
  • 13. • Armの可変長SIMD命令セット • 128~1024(?)bitレジスタ • double x 8, float x 16, int32 x 16, int16 x 32, int8 x 64などの型 • 概ね3オペランド • A64FX(富岳のCPU)では32個の512bitレジスタz0, ..., z31 • movprfx(dstをsrcとして利用) • predは述語レジスタ • マスクレジスタ相当 • AVX-512と異なり常に指定 SVE fadd z0.s, z1.s, z2.s // as float add z0.b, z1.b, z2.b // as int8 movprfx(dst, pred, r1); fmadd(dst, pred, r2, r3); // dst = r1 * r2 + r3 13 / 27
  • 14. • SVEレジスタの各要素を処理する(1)か否(0)かを指定 • 例 ld1w(z.s, p/T_z, ptr(src)); • z = *src; // float x 16個読み込み • i番目の各要素(i = 0, ..., 15)について • 述語レジスタp[i] = 1ならz[i] = src1[i] • p[i] = 0ならT_z(zero)を指定しているのでz[i] = 0 • T_zを指定しなければp[i]の値を変更しない 述語レジスタ src x0 x1 x2 x3... z.s x0 0 x2 x3... p 1 0 1 1 14 / 27
  • 15. • 「x4 + i < n」が成り立つ添え字までp[i] = 1にする • 例 x4 + 16 <= nならi = 0, ..., 15についてp[i] = 1 • 全てのデータが有効 • x4 + 3 = nならi ≦ 2についてp[i] = 1, その他p[i] = 0 • p[i] = 0の部分はデータを読まない・書かない • 読み書き属性が無い領域でも大丈夫 whilelt(p.s, x4, n); i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 p 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 p 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 15 / 27
  • 16. • メイン部分 • x4 + 16 ≦ nである限りp0[i] = 1 for i = 0, ..., 15 • ループの最終ではp0[i] = 1 for i <(n % 16), p0[i] = 0(otherwise) • メリット • SVEが256bitや1024bitでも同じコードで動く • SVEはScalable Vector Extensionの略 • デメリット • あまり速くない ; 結局AVX-512のように分けた方がよい ループの終わり処理 Label lp = L(); ld1w(z0.s, p0/T_z, ptr(src1, x4, LSL, 2));// z0 = src1[x4 << 2] ... incd(x4); // x4 += 16 L(cond); whilelt(p0.s, x4, n); // while (x4 < n)なら b_first(lp); // lpラベルにジャンプ 16 / 27
  • 17. • AVX-512とほぼ同じ • 述語レジスタpはall 1に設定 • round関数はfrintn • fscale(x, p, n)のnがfloatではなくintなのでfcvtzsでintに変換 sveによるgenExpOne fmul(tz0, tz0, log2_e); frintn(tz2, p, tz0); // round : float -> float fcvtzs(tz1, p, tz2); // float -> int fsub(tz2, tz0, tz2); fmul(tz2, tz2, log2); movprfx(tz0, p, E); fmad(tz0, p, tz2, D); // tz0 = tz2 * E + D fmad(tz0, p, tz2, C); fmad(tz0, p, tz2, B); fmad(tz0, p, tz2, one); fmad(tz0, p, tz2, one); fscale(tz0, p, tz1); // tz0 *= 2^tz1 17 / 27
  • 18. • frintnなどの命令のあとの依存関係 • レジスタ名を入れ換えると2倍速くなるケース(74→32nsec) • https://github.com/herumi/misc/commit/0362d5647f693be66 da841eea7ca333d0f5b5329 レジスタリネーミングが苦手? 18 / 27
  • 19. • 利用レジスタを増やしてループ展開 ループアンロール // n = 1 or 2 or 3 for (int i = 0;i<n;i++) fmul(t[0+i*3], t[0+i*3], log2_e); for (int i = 0;i<n;i++) frintn(t[2+i*3], p, t[0+i*3]); for (int i = 0;i<n;i++) fcvtzs(t[1+i*3], p, t[2+i*3]); for (int i = 0;i<n;i++) fsub(t[2+i*3], t[0+i*3], t[2+i*3]); for (int i = 0;i<n;i++) fmul(t[2+i*3], t[2+i*3], log2); for (int i = 0;i<n;i++) { movprfx(t[0+i*3], p, E); fmad(t[0+i*3], p, t[2+i*3], D); } for (int i = 0;i<n;i++) fmad(t[0+i*3], p, t[2+i*3], C); for (int i = 0;i<n;i++) fmad(t[0+i*3], p, t[2+i*3], B); for (int i = 0;i<n;i++) fmad(t[0+i*3], p, t[2+i*3], one); for (int i = 0;i<n;i++) fmad(t[0+i*3], p, t[2+i*3], one); for (int i = 0;i<n;i++) fscale(t[0+i*3], p, t[1+i*3]); 19 / 27
  • 20. • float x[16384];に対するstd::exp(float)との比較(nsec) • FX700 • A64FXはXeonに比べてレイテンシが大きい • https://github.com/fujitsu/A64FX • frintn, fmul, fadd; 9clk • fdiv, 98clk • ループアンロールの効果が大きい ベンチマーク std::exp fmath::exp, N=1 N=2 N=3 217.3 19.0 11.8 8.4 20 / 27
  • 21. • pow(2, i/64) (i=0,...,63)のテーブル引き命令 • floatのbit表現 • u=|s:1|e:8|f:23|, 𝑓 = −1 𝑠 2 𝑒−127 (1 + 𝑓 224), e:指数部, f:仮数部 • tbl[i] := 「pow(2, i/64)」の下位23bit • 𝑥 = 2 𝑖 64 for 𝑖 = 0, … , 63は1 ≤ 𝑥 < 2なので常にx.e=127 • fexp(x)の挙動 SVEのfexpa x.s x.e x.f v=6bity=8bit tbl[] tbl[v]y=8bit0 21 / 27
  • 22. • 2ベキの近似なのでそれにもっていく • exp 𝑥 = 2 𝑥 log2 𝑒 = 2 𝑦 where 𝑦 ≔ 𝑥 log2 𝑒 • 𝑦 = 𝑛 + 𝑎 where 𝑛 ≔ floor(𝑦), 𝑎 ≔ 𝑛 − 𝑦, 0 ≤ 𝑎 < 1 • fexpaの添え字vが2ベキになるには[1, 2)にある必要性 • 𝑏 ≔ 1 + 𝑎とすると1 ≤ 𝑏 < 2, 𝑦 = 𝑛 − 1 + 𝑏 • 𝑏.f上位6bit vを取り出して使う c = fexpa(b.f >> 17) • 𝑏 = 𝑏. f/224=v /26 + w/224, 𝑧 ≔ w/224 • 2 𝑏 =fexpa(v)⋅ 2 𝑧, 2 𝑧 = 𝑒log 2 𝑧 = 1 + log 2 𝑧 + log 2 𝑧 2/2 • 差分が小さいので2次の項まででよい • https://github.com/herumi/misc/blob/master/sve/fmath-sve.hpp fexpaの使い方 𝑏.f v=6bit w=17bit 22 / 27
  • 23. • 主要コード概要 fexpaによるexp fmul(t0, t0, para.log2_e); frintm(t1, p, t0); // floor : float -> float fcvtzs(t2, p, t1); // n = float -> int fsub(t1, t0, t1); // a fadd(t1, t1, one); // b = 1 + a lsr(t3, t1, 17); // v = b >> 17 fexpa(t3, t3); // c = fexpa(v) fscale(t3, p, t2); // t3 *= 2^n and_(t2.d, t1.d, not_mask17.d); fsub(t2, t1, t2); // z movprfx(t0, p, coeff2); fmad(t0, p, t2, coeff1); fmad(t0, p, t2, one); // 1 + log2 z + (log2)^2/2 z^2 fmul(t0, t3, t0); 23 / 27
  • 24. • float x[16384];に対するstd::exp(float)との比較(nsec) • ()内はfxpaを使わないバージョン • 気持ち速いかも? • 定数レジスタの個数 7 vs 5 • 中間レジスタの個数 3 vs 4 • 雑感 • 最初の方法に比べてレジスタの依存関係が複雑になる • 定数レジスタが少ないのはよいがそこまでのメリットが • もう少し精度があれば • うまく速くなるレジスタ割り当てに試行錯誤(全数探索?) ベンチマーク std::exp N=1 N=2 N=3 217.3 18.2 (19.0) 11.3 (11.8) 8.4 (8.4) 24 / 27
  • 25. • fdivの代わりに逆数近似命令(rcp)のあと補正する • AVX-512 • SVE ; frecps(dst, src1, src2) ; // dst = 2 - src1 * src2 • 2回補正するとfloatに近い精度 逆数近似計算 t = rcp(x) 1/x = 2 * t - x t^2 // x : input/output // t : temporary // two : 2.0f vrcp14ps(t, x); vfnmadd213ps(x, t, two); // x = -xt + 2 vmulps(x, x, t); 25 / 27
  • 26. • frintmあるいはaddの後のz0の逆数処理 • https://github.com/herumi/misc/blob/master/sve/inv.cpp • ○で囲った部分が変? • レジスタリネーミング? 逆数近似計算 // input : z0 (compute it with z0, z1, z2) 1. fdivr(z0.s, p0, one.s); 2. frecpe(z1.s, z0.s); frecps(z2.s, z0.s, z1.s); fmul(z0.s, z1.s, z2.s); 3. frecpe(z1.s, z0.s); frecps(z3.s, z0.s, z1.s); fmul(z0.s, z1.s, z3.s); 4. frecpe(z1.s, z0.s); frecps(z2.s, z0.s, z1.s); fmul(z1.s, z1.s, z2.s); frecps(z2.s, z0.s, z1.s); fmul(z0.s, z1.s, z2.s); 5. frecpe(z1.s, z0.s); frecps(z3.s, z0.s, z1.s); fmul(z1.s, z1.s, z3.s); frecps(z3.s, z0.s, z1.s); fmul(z0.s, z1.s, z3.s); clk frintm add 100 100 33 7.9 10.9 7.9 51 11.0 11.2 11.0 26 / 27
  • 27. • AVX-512とSVE(A64FX) • どちらも512-bitレジスタx32 • SVEの方がレイテンシが大きい • ループアンロール重要 • レジスタリネーミングに気をつける場合がある? まとめ 27 / 27