Slide 1

Slide 1 text

ISSP, Univ. of Tokyo 1/36 「京」におけるマルチスレッドmalloc 渡辺宙志 東京大学物性研究所 2018年1月24日「メニーコア時代のアプリ性能検討WG第5回会合」@汐留 ※ メニーコア時代のアプリ性能検討WG 成果報告書を参照 http://www.ssken.gr.jp/MAINSITE/download/wg_report/mcap/index.html

Slide 2

Slide 2 text

ISSP, Univ. of Tokyo 2/36 Process Process Process Process Process Process Process Process Flat-MPI CPUコアそれぞれにプロセスを割り当てる 各CPUコア間で直接通信を行う 利点:プログラムが楽 欠点:プロセス数のニ乗でメモリ使用量が増える → 「京」フルノードflat-MPIはは不可能 疑似flat-MPI法 (1/2)

Slide 3

Slide 3 text

ISSP, Univ. of Tokyo 3/36 Thread Thread Thread Thread Process Process 通信 Thread Thread Thread Thread 擬似Flat-MPI法 プロセスもスレッドも領域分割してしまう 疑似flat-MPI法 (2/2) 利点: ・ループ分割に比べればプログラムが楽 ・スレッド並列とSIMD化を同時に考えなくて良い 欠点: ・通信がやや面倒くさい cf. HW, M. Suzuki, and N. Ito: Comput. Phys. Commun. 184 2775-2784 (2013)

Slide 4

Slide 4 text

ISSP, Univ. of Tokyo 4/36 ・x86系ではflat-MPIとハイブリッドで性能差はほぼ無い ・「京」やFX10ではハイブリッド実行が有意に遅い ハイブリッド計算の性能 通信まわり以外はflat-MPIとハイブリッド計算は全く同じ計算を行う → 通信が無視できる状況では性能差は出ないはず Process Process Process Process Process Process Process Process Thread Thread Thread Thread Process Process 通信 Thread Thread Thread Thread 同じコアには同じ計算領域が割り当てられ、全く同じワーク ロードを担当する ハイブリッド実行時の性能劣化原因について調べる flat-MPI ハイブリッド

Slide 5

Slide 5 text

ISSP, Univ. of Tokyo 5/36 ・ベンチマーク ・領域あたり62500粒子*8並列=50万粒子 ・カットオフ2.5, 時間刻み0.001 ・1000ステップにかかった時間 ・シングルノード計算 Flat-MPI : 8プロセス×1スレッド 54.2537 [sec] OpenMP: 1プロセス ×8スレッド 58.0699 [sec] 数%ほどOpenMPが有意に遅い (昔は10〜15%ほど遅かったが、改善された?) flatMPI OpenMP lower is better 観測事実1:シングルノード性能 (1/2)

Slide 6

Slide 6 text

ISSP, Univ. of Tokyo 6/36 観測事実1:シングルノード性能 (2/2) 計算の構造 Thread Thread Thread Thread Main Thread Main Thread 力の計算 位置の更新 位置情報の通信 Thread Thread Thread Thread ペアリスト作成 その後、遅くなる場所がペアリスト作成ルーチン内の std::vectorの宣言部であることが判明 ココ

Slide 7

Slide 7 text

ISSP, Univ. of Tokyo 7/36 thread_localの効果 (1/3) コードの構造 #pragma omp parallel for for(int i=0;i v; //... } スレッドが独立にある関数を呼ぶ その関数内にstd::vectorがある このstd::vectorがスレッドごとに独立に触られることが コンパイラには判断できない? スレッドローカル指定してみる

Slide 8

Slide 8 text

ISSP, Univ. of Tokyo 8/36 ソースの一行だけ書き換える (ペアリスト作成用一時配列) ホットスポットと無関係な宣言一行の変更で10%以上高速化 thread_localの効果 (2/3) flatMPI OpenMP OpenMP + TLS lower is better thread_local std::vector v; v.clear(); std::vector v;

Slide 9

Slide 9 text

ISSP, Univ. of Tokyo 9/36 thread_localの効果 (3/3) 当初の理解 スレッドローカル指定により、複数のスレッドが同じstd::vectorを 触らないことがわかるため、排他制御が不要になって早くなった よく考えるとおかしい 実際の仕組み ・ 関数内のローカル変数は自動的にスレッドローカルになる ・ std::vectorへのポインタがスレッドローカルだとしても、内部から 呼ばれるmallocはヒープに取るからどうせ排他制御必須 std::vector v; std::vector v; std::vector v; スレッド1 スレッド2 スレッド3 ヒープ ではなぜ早くなった? (後半へ続く)

Slide 10

Slide 10 text

ISSP, Univ. of Tokyo 10/36 lower is better シングルノードでの性能は改善された しかし、マルチノード実行でハイブリッドの方が有意に遅い ハイブリッド flat-MPI 高並列時、ハイブリッドはflat-MPIに比べて20%くらい遅い 観測事実2:マルチノード性能 (1/3) シングルノード計算と同条件で ウィークスケーリング

Slide 11

Slide 11 text

ISSP, Univ. of Tokyo 11/36 XOS_MMM_L_ARENA_LOCK_TYPEとは TYPE=1 メモリ効率優先 (mallocをシリアル処理) [デフォルト] TYPE=0 メモリ確保速度優先 (mallocを並列処理) もしマルチスレッドにおける「mallocのシリアル処理」が遅いのなら、 メモリ確保速度優先にすれば性能が向上するはず。 XOS_MMM_L_ARENA_LOCK_TYPEをいじる 昔は速度優先がデフォルトだったが、ある時からメモリ効率優先に変更したらしい 観測事実2:マルチノード性能 (2/3)

Slide 12

Slide 12 text

ISSP, Univ. of Tokyo 12/36 lower is better 速度優先にしたら劇的に遅くなった。速度ブレもひどい。 flat-MPI ハイブリッド(効率優先) ハイブリッド(速度優先) 観測事実2:マルチノード性能 (3/3) lock 1 → 0

Slide 13

Slide 13 text

ISSP, Univ. of Tokyo 13/36 遅くなっているのは「スレッド並列が終わった後のシリアル処理部」 Thread Thread Thread Thread Main Thread Main Thread 力の計算 位置の更新 位置情報の通信 ・遅い場所はやはりstd::vectorを含むルーチン ・今回はスレッド並列実行をしていないのでthread_localが無意味 ・x86系ではそんなところが遅くなったことはない 性能劣化の場所と条件 (1/2)

Slide 14

Slide 14 text

ISSP, Univ. of Tokyo 14/36 ・スレッド並列リージョンでstd::vectorからmallocが呼ばれ、 ・その後のシリアルリージョンで全く別のstd::vectorを触り、 ・そのstd::vectorの先頭アドレスをMPI_Sendrecvに渡して通 信しようとすると、 ・通常数ミリ秒で終わる通信が、200ミリ秒かかることがあるの が性能劣化原因 性能劣化の場所と条件 (2/2) 性能劣化条件 その後のヘルプデスクやRISTの努力により、以下のことがわかった なぜ? mallocの仕組みを知らないとこれ以上は調査不可能 mallocの仕組みから調べなおす

Slide 15

Slide 15 text

ISSP, Univ. of Tokyo 15/36 mallocの仕組み (1/6) メモリ空間 ユーザプログラムが自由に使える領域 ・スタック領域 ・ヒープ領域 ・mmapで確保した領域 プログラム領域 データ領域 ヒープ領域 未使用領域 スタック領域 拡 張 mmap領域 mmap領域 ユーザプログラムからOSへのメモリ要求は ・ ヒープ拡張 (sbrk) ・ mmap 呼び出し というシステムコールで行われる。 どちらも遅いので、プログラムは一度確保した領域を なるべく使いまわす→ malloc

Slide 16

Slide 16 text

ISSP, Univ. of Tokyo 16/36 mallocの仕組み (2/6) アリーナとチャンク ・ユーザからメモリ確保要求があると必要に応じてヒープを拡張 ・ヒープからメモリを切り出してユーザに返す(チャンク) ・現在どこを使っているか管理する(アリーナ) ヒープ ヒープ ヒープ チャンク 最初はヒープ領域はゼロ ユーザ ヒープを拡張 チャンクを切り出して そのアドレスをユーザに返す mallocが管理するメモリ領域の単位をチャンクと呼ぶ チャンクを管理する単位をアリーナと呼ぶ アリーナ アリーナ

Slide 17

Slide 17 text

ISSP, Univ. of Tokyo 17/36 mallocの仕組み (3/6) チャンクリスト ・ユーザから開放されたチャンクをリストで管理 ・次回のメモリ確保要求でチャンクをリサイクル アリーナ 使用中 空き 使用中 空き 空き 最後にfreeされた チャンクへのリンク メモリ要求 アリーナ 使用中 空き 使用中 空き 使用中 最後にfreeされた チャンクへのリンク このチャンクを ユーザに返す

Slide 18

Slide 18 text

ISSP, Univ. of Tokyo 18/36 mallocの仕組み (4/6) リストのデフラグ ・空きチャンクの整理 (malloc_consolidate) 使用中 空き 空き 空き 使用中 空き 空き 空き 使用中 空き 使用中 空き メモリが断片化し、本来なら確保で きるメモリ要求に対応できない このサイズ を要求 デフラグ デフラグしてから空き領域を返す

Slide 19

Slide 19 text

ISSP, Univ. of Tokyo 19/36 mallocの仕組み (5/6) mmappedチャンク ・大きなチャンクの確保 (mmapped chunk) 大きなメモリ を要求 使用中 空き 空き 空き 使用中 空き 空き 空き 使用中 mmapで確保 して返す 小さなメモリ を要求 アリーナ(ヒープ) ヒープから 探して返す 大きいチャンクはヒープから探さずにmmapで確保して返す

Slide 20

Slide 20 text

ISSP, Univ. of Tokyo 20/36 マルチスレッド処理 アリーナ(ヒープ) 使用中 空き 使用中 空き 空き アリーナ管理領域 スレッド スレッド 競合 アリーナ管理領域 新たにアリーナを確保 (mmap) mallocの仕組み (6/6) 空き 使用中 アリーナ(mmap)

Slide 21

Slide 21 text

ISSP, Univ. of Tokyo 21/36 サイズ ユーザ領域 チャンクヘッダ mallocが 返してくる ポインタ malloc直後 free直後 サイズ 前方へのリンク 後方へのリンク チャンクサイズ64バイトまで:前方参照リンク チャンクサイズ64バイトから:チャンク結合+双方向リンク (glibc mallocは128バイトから) チャンクの構造 freeされると、チャンクには管理用の情報が書き込まれる チャンクの構造

Slide 22

Slide 22 text

ISSP, Univ. of Tokyo 22/36 mallocしてfreeしてチャンクリストを表示 自明にまとめられたチャンクは非表示 #include #include #include const int N = 128; int main(int argc, char **argv) { char *buf[N]; size_t size = 128; #pragma omp parallel for for (int i = 0; i < N; i++) { buf[i] = (char*)malloc(size); } for (int i = 0; i < N; i++) { for (int j = 0; j < size; j++) { buf[i][j] = 0; } } for (int i = 0; i < N; i++) { free(buf[i]); } printf("digraph test_%d¥n {¥n", size); for (int i = 0; i < N; i++) { size_t *p = (size_t*)(buf[i]); size_t prev_size = *(p - 2); if (prev_size != 0)continue; size_t *ct = p - 2; size_t fd = *p; printf("¥"%lx¥"->¥"%lx¥"¥n", ct, fd); } printf(")¥n"); } https://gist.github.com/kaityo256/32bc425b630642f67e649854104f977e 128バイトを128個malloc&free malloc のチャンクリスト形状 (1/5) チャンクリスト可視化コードを書いた 注: デフラグされなかったチャンクを表示しているわけではない 隣接するチャンクがまとめられた時、代表チャンクのみ表示

Slide 23

Slide 23 text

ISSP, Univ. of Tokyo 23/36 物性研 (Xeon E5 2680 12core × 2ソケット) malloc のチャンクリスト形状 (2/5) ヒープに取られているチャンクも、 mmapに取られているチャンクもある mmapで確保 ヒープ 24スレッド実行

Slide 24

Slide 24 text

ISSP, Univ. of Tokyo 24/36 LOCK_TYPE=1 (メモリ獲得のシリアライズ化をする) これがデフォルト XOS_MMM_L_ARENA_LOCK_TYPEの効果(1/3) メモリチャンクリストがごちゃごちゃしている 全てヒープに確保している(mmapを呼んでいない) 京 (8 core × 1ソケット) 8スレッド実行

Slide 25

Slide 25 text

ISSP, Univ. of Tokyo 25/36 LOCK_TYPE=0 (メモリ獲得のシリアライズ化をしない) XOS_MMM_L_ARENA_LOCK_TYPEの効果(2/3) 京 (8 core × 1ソケット) 8スレッド実行 mmapが呼ばれている mmapで確保 ※ もともとの目的は、malloc_consolidateが遅いのではと考えて、チャンクリスト 構造を可視化することだったが、mmapの有無が大きな原因とわかった

Slide 26

Slide 26 text

ISSP, Univ. of Tokyo 26/36 XOS_MMM_L_ARENA_FREE とは (1.7ラージページチュートリアル) FREE=1 メモリ効率優先 (メモリページを即解放) [デフォルト] FREE=2 速度優先 (メモリページを解放しない) XOS_MMM_L_ARENA_FREE をいじる XOS_MMM_L_ARENA_LOCK_TYPE =0の時に遅いのは mmapが原因? XOS_MMM_L_ARENA_FREEの効果(1/2) メモリページを解放しないことで、mmap/munmapのコストが消えることを期待 → スレッドヒープの活用により、チャンクリストも短くなるはず

Slide 27

Slide 27 text

ISSP, Univ. of Tokyo 27/36 XOS_MMM_L_ARENA_FREEの効果(2/2) ハイブリッド(速度優先+解放有) free 1 → 2 ハイブリッド(効率優先) ハイブリッド(速度優先+解放無) 速度優先(LOCK_TYPE=0)で遅くなった分が元に戻った LOCK_TYPE=0より早くなることを期待したが、ほぼ同じに → スレッドヒープを利用したのならconsolidateのコストが軽減するはず まだ何かおかしい・・・

Slide 28

Slide 28 text

ISSP, Univ. of Tokyo 28/36 #include #include #include const int N = 128; int main(void){ char *buf[N]; #pragma omp parallel for for(int i=0;i

Slide 29

Slide 29 text

ISSP, Univ. of Tokyo 29/36 期待していた動作: LOCK_TYPE=0かつARENA_FREE=1 → mmapped arenaが使われ、かつ領域違反 LOCK_TYPE=0かつARENA_FREE=2 → mmapped arenaが使われ、かつ領域違反せず 実際の動作: LOCK_TYPE=0かつARENA_FREE=1 → mmapped arenaが使われ、かつ領域違反 LOCK_TYPE=0かつARENA_FREE=2 → mmapped arenaが使われない(なので領域違反も起きない) マニュアル(1.7ラージページチュートリアル)によると 「1」の場合、mmappedチャンクに確保された領域は即時解放される。 「2」は(中略) スレッド固有ヒープは使用しない。 → mmapped アリーナを解放するんじゃなくて? → なぜ? ARENA_FREEの実際の効果(2/3)

Slide 30

Slide 30 text

ISSP, Univ. of Tokyo 30/36 ARENA_FREEの実際の効果(3/3) 環境変数の名前がおかしい? XOS_MMM_L_ARENA_FREEという名前から、 ・ 「アリーナ」の解放に関する変数であり ・ mmapされたスレッドヒープをmunmapしない動作を期待 実際の動作は ・「チャンク」の解放に関する変数 ・指定されると大きなチャンクをmmapで取らなくなる ・指定されるとスレッドヒープも使わなくなる(LOCK_TYPEを上書き) 環境変数の機能が直交していない? mmappedチャンクを作りたくなければMALLOC_MMAP_THREASHOLDをいじるべき XOS_MMM_L_ARENA_LOCK_TYPE → XOS_USE_MMAPPED_ARENA XOS_MMM_L_ARENA_FREE → XOS_USE_MMAPPED_CHUNK こういう名前の方が分かりやすかった・・・

Slide 31

Slide 31 text

ISSP, Univ. of Tokyo 31/36 thread_localの効果 (revisited) thread_localの効果 ・グローバル変数をスレッドローカルにする ・暗黙のstatic指定がつく #pragma omp parallel for for(int i=0;i v; //... //... } malloc free thread_local指定無し→ 関数が呼ばれるとmalloc、抜ける時にfree thread_local指定 → std::vectorがstatic変数に(スコープが制限されたグローバル変数) → 最初の一度だけmalloc、プログラム終了時にfree → freeの回数が減る (チャンクリストのお片付け回数が減る) → 早くなる ※ flat-MPIの場合も毎回freeが走っていたが、チャンクリストが短かった?

Slide 32

Slide 32 text

ISSP, Univ. of Tokyo 32/36 なぜハイブリッドではstd::vectorが遅くなるか? おそらくチャンクのリンクリストが長くなるのが原因 プロセス プロセス flat-MPI ハイブリッド プロセス 各プロセスが独自のアリーナを保持 プロセスあたりのメモリ空間が小さい 各アリーナのリンクリストも短く単純 複数のプロセスが一つのアリーナを共有 プロセスあたりのメモリ空間が大きい リンクリストが長く複雑に malloc_consolidateが遅いのが本質?

Slide 33

Slide 33 text

ISSP, Univ. of Tokyo 33/36 ハイブリッド高並列時の性能劣化 (1/2) static指定 Thread Thread Thread Thread Main Thread Main Thread 力の計算 位置の更新 位置情報の通信 thread_localと同様なことが起きているのでは? シリアル部のstd::vector宣言部にstatic指定してみる 効果なし 関数ローカルだったstd::vectorが毎回freeされなくなる → デフラグが走らなくなることで高速化を期待

Slide 34

Slide 34 text

ISSP, Univ. of Tokyo 34/36 eager-limit指定 MPIでは小さい通信はバッファにコピーされて通信する(eager通信) 効果なし eager (私書箱方式) DATA sender ハイブリッド高並列時の性能劣化 (2/2) eagerプロトコルにおいて、MPIライブラリ内部でmallocを 呼んでいて、それが悪さをしているのでは? receiver Buffer DATA コピー 送信 Rendezvous (直接手渡し) DATA sender receiver Buffer 送信側は受信側を待たない 受信側は都合の良い時に受け取る 送信側は受信側の準備が整ったら送信 eager通信のサイズ上限をゼロにして 強制的にランデブー通信指定

Slide 35

Slide 35 text

ISSP, Univ. of Tokyo 35/36 まとめ シングルノード実行でマルチスレッドが遅い問題 関数ローカルに宣言されたstd::vectorをthread_localにすることで回避 高並列実行時にマルチスレッドが遅い問題 LOCK_TYPE=0にしたら劇的に遅くなるのはおそらくmmap/munmapのコスト ・ 京はデフォルトでスレッド競合時にmmapしない ・mmappedアリーナを使うと恐ろしく遅くなる → mmapped アリーナを利用し、かつmunmapしない組み合わせなら早い? LOCK_TYPE=1の時にflat-MPIより遅い原因は不明 おそらくチャンクリストが複雑になり、malloc_consolidate が遅くなるのが原因だと思われるが、MPIの実装(特にマ ルチスレッド対応まわり)に問題がある可能性も 本質はstatic宣言により、毎回呼ばれていたmalloc/freeが消えたこと flat-MPIで顕在化しなかった理由は、チャンクリストが短く単純だったから?

Slide 36

Slide 36 text

ISSP, Univ. of Tokyo 36/36 疑問点・問題点 問題が起きた時のため、積極的な情報公開を希望する 「京」ではマルチスレッド+STLコンテナの組み合わせは 想定されていない? ウェブに情報が少なすぎる 「OMPI_MCA_btl_tofu_eager_limit」の検索結果 1件 「XOS_MMM_L_ARENA_LOCK_TYPE」の検索結果 4件 「XOS_MMM_L_ARENA_FREE」の検索結果 7件 munmapやmalloc_consolidateが遅い?本当ならなぜ? x86系で問題になったことが無い。 IntelコンパイラのOpenMP実装で使われるkmp_malloc/freeが賢い? GCC実装は調べてないので不明