Upgrade to Pro — share decks privately, control downloads, hide ads and more …

「京」におけるマルチスレッドmalloc / malloc on K computer

「京」におけるマルチスレッドmalloc / malloc on K computer

メニーコア時代のアプリ性能検討WGにおける報告資料。以下も参照
http://www.ssken.gr.jp/MAINSITE/download/wg_report/mcap/index.html

A10e41b0a61d59f2258d7f6172c33479?s=128

kaityo256
PRO

January 24, 2018
Tweet

More Decks by kaityo256

Other Decks in Programming

Transcript

  1. 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
  2. ISSP, Univ. of Tokyo 2/36 Process Process Process Process Process

    Process Process Process Flat-MPI CPUコアそれぞれにプロセスを割り当てる 各CPUコア間で直接通信を行う 利点:プログラムが楽 欠点:プロセス数のニ乗でメモリ使用量が増える → 「京」フルノードflat-MPIはは不可能 疑似flat-MPI法 (1/2)
  3. 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)
  4. 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 ハイブリッド
  5. 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)
  6. ISSP, Univ. of Tokyo 6/36 観測事実1:シングルノード性能 (2/2) 計算の構造 Thread Thread

    Thread Thread Main Thread Main Thread 力の計算 位置の更新 位置情報の通信 Thread Thread Thread Thread ペアリスト作成 その後、遅くなる場所がペアリスト作成ルーチン内の std::vectorの宣言部であることが判明 ココ
  7. ISSP, Univ. of Tokyo 7/36 thread_localの効果 (1/3) コードの構造 #pragma omp

    parallel for for(int i=0;i<thread_num;i++){ func(i); } void func(int thread_index){ std::vector<int> v; //... } スレッドが独立にある関数を呼ぶ その関数内にstd::vectorがある このstd::vectorがスレッドごとに独立に触られることが コンパイラには判断できない? スレッドローカル指定してみる
  8. ISSP, Univ. of Tokyo 8/36 ソースの一行だけ書き換える (ペアリスト作成用一時配列) ホットスポットと無関係な宣言一行の変更で10%以上高速化 thread_localの効果 (2/3)

    flatMPI OpenMP OpenMP + TLS lower is better thread_local std::vector<int> v; v.clear(); std::vector<int> v;
  9. ISSP, Univ. of Tokyo 9/36 thread_localの効果 (3/3) 当初の理解 スレッドローカル指定により、複数のスレッドが同じstd::vectorを 触らないことがわかるため、排他制御が不要になって早くなった

    よく考えるとおかしい 実際の仕組み ・ 関数内のローカル変数は自動的にスレッドローカルになる ・ std::vectorへのポインタがスレッドローカルだとしても、内部から 呼ばれるmallocはヒープに取るからどうせ排他制御必須 std::vector<int> v; std::vector<int> v; std::vector<int> v; スレッド1 スレッド2 スレッド3 ヒープ ではなぜ早くなった? (後半へ続く)
  10. ISSP, Univ. of Tokyo 10/36 lower is better シングルノードでの性能は改善された しかし、マルチノード実行でハイブリッドの方が有意に遅い

    ハイブリッド flat-MPI 高並列時、ハイブリッドはflat-MPIに比べて20%くらい遅い 観測事実2:マルチノード性能 (1/3) シングルノード計算と同条件で ウィークスケーリング
  11. 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)
  12. ISSP, Univ. of Tokyo 12/36 lower is better 速度優先にしたら劇的に遅くなった。速度ブレもひどい。 flat-MPI

    ハイブリッド(効率優先) ハイブリッド(速度優先) 観測事実2:マルチノード性能 (3/3) lock 1 → 0
  13. ISSP, Univ. of Tokyo 13/36 遅くなっているのは「スレッド並列が終わった後のシリアル処理部」 Thread Thread Thread Thread

    Main Thread Main Thread 力の計算 位置の更新 位置情報の通信 ・遅い場所はやはりstd::vectorを含むルーチン ・今回はスレッド並列実行をしていないのでthread_localが無意味 ・x86系ではそんなところが遅くなったことはない 性能劣化の場所と条件 (1/2)
  14. ISSP, Univ. of Tokyo 14/36 ・スレッド並列リージョンでstd::vectorからmallocが呼ばれ、 ・その後のシリアルリージョンで全く別のstd::vectorを触り、 ・そのstd::vectorの先頭アドレスをMPI_Sendrecvに渡して通 信しようとすると、 ・通常数ミリ秒で終わる通信が、200ミリ秒かかることがあるの

    が性能劣化原因 性能劣化の場所と条件 (2/2) 性能劣化条件 その後のヘルプデスクやRISTの努力により、以下のことがわかった なぜ? mallocの仕組みを知らないとこれ以上は調査不可能 mallocの仕組みから調べなおす
  15. ISSP, Univ. of Tokyo 15/36 mallocの仕組み (1/6) メモリ空間 ユーザプログラムが自由に使える領域 ・スタック領域

    ・ヒープ領域 ・mmapで確保した領域 プログラム領域 データ領域 ヒープ領域 未使用領域 スタック領域 拡 張 mmap領域 mmap領域 ユーザプログラムからOSへのメモリ要求は ・ ヒープ拡張 (sbrk) ・ mmap 呼び出し というシステムコールで行われる。 どちらも遅いので、プログラムは一度確保した領域を なるべく使いまわす→ malloc
  16. ISSP, Univ. of Tokyo 16/36 mallocの仕組み (2/6) アリーナとチャンク ・ユーザからメモリ確保要求があると必要に応じてヒープを拡張 ・ヒープからメモリを切り出してユーザに返す(チャンク)

    ・現在どこを使っているか管理する(アリーナ) ヒープ ヒープ ヒープ チャンク 最初はヒープ領域はゼロ ユーザ ヒープを拡張 チャンクを切り出して そのアドレスをユーザに返す mallocが管理するメモリ領域の単位をチャンクと呼ぶ チャンクを管理する単位をアリーナと呼ぶ アリーナ アリーナ
  17. ISSP, Univ. of Tokyo 17/36 mallocの仕組み (3/6) チャンクリスト ・ユーザから開放されたチャンクをリストで管理 ・次回のメモリ確保要求でチャンクをリサイクル

    アリーナ 使用中 空き 使用中 空き 空き 最後にfreeされた チャンクへのリンク メモリ要求 アリーナ 使用中 空き 使用中 空き 使用中 最後にfreeされた チャンクへのリンク このチャンクを ユーザに返す
  18. ISSP, Univ. of Tokyo 18/36 mallocの仕組み (4/6) リストのデフラグ ・空きチャンクの整理 (malloc_consolidate)

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

    chunk) 大きなメモリ を要求 使用中 空き 空き 空き 使用中 空き 空き 空き 使用中 mmapで確保 して返す 小さなメモリ を要求 アリーナ(ヒープ) ヒープから 探して返す 大きいチャンクはヒープから探さずにmmapで確保して返す
  20. ISSP, Univ. of Tokyo 20/36 マルチスレッド処理 アリーナ(ヒープ) 使用中 空き 使用中

    空き 空き アリーナ管理領域 スレッド スレッド 競合 アリーナ管理領域 新たにアリーナを確保 (mmap) mallocの仕組み (6/6) 空き 使用中 アリーナ(mmap)
  21. ISSP, Univ. of Tokyo 21/36 サイズ ユーザ領域 チャンクヘッダ mallocが 返してくる

    ポインタ malloc直後 free直後 サイズ 前方へのリンク 後方へのリンク チャンクサイズ64バイトまで:前方参照リンク チャンクサイズ64バイトから:チャンク結合+双方向リンク (glibc mallocは128バイトから) チャンクの構造 freeされると、チャンクには管理用の情報が書き込まれる チャンクの構造
  22. ISSP, Univ. of Tokyo 22/36 mallocしてfreeしてチャンクリストを表示 自明にまとめられたチャンクは非表示 #include <cstdio> #include

    <cstdlib> #include <omp.h> 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) チャンクリスト可視化コードを書いた 注: デフラグされなかったチャンクを表示しているわけではない 隣接するチャンクがまとめられた時、代表チャンクのみ表示
  23. ISSP, Univ. of Tokyo 23/36 物性研 (Xeon E5 2680 12core

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

    全てヒープに確保している(mmapを呼んでいない) 京 (8 core × 1ソケット) 8スレッド実行
  25. 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の有無が大きな原因とわかった
  26. 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のコストが消えることを期待 → スレッドヒープの活用により、チャンクリストも短くなるはず
  27. ISSP, Univ. of Tokyo 27/36 XOS_MMM_L_ARENA_FREEの効果(2/2) ハイブリッド(速度優先+解放有) free 1 →

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

    <omp.h> const int N = 128; int main(void){ char *buf[N]; #pragma omp parallel for for(int i=0;i<N;i++){ buf[i] = (char*)malloc(128); } for(int i=0;i<N;i++){ size_t *p = (size_t*)buf[i]; if (*(p-1) & 4){ printf("mmapped arena is used.¥n"); goto label; } } label: for(int i=0;i<N;i++){ free(buf[i]); buf[i][0] = 0; } } こんなコードを書いてみる OpenMPスレッド並列でmalloc mmapされたアリーナが即解放されるなら、その後で触ると領域違反が起きるはず mmapされたアリーナが解放されないなら、その後で触っても領域違反は起きない free後に確保された領域を触る mmapped arenaが使われているか調べる ARENA_FREEの実際の効果(1/3) https://gist.github.com/kaityo256/40790bc3ce51b2235e3ae9ca3d99eeef
  29. 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)
  30. 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 こういう名前の方が分かりやすかった・・・
  31. ISSP, Univ. of Tokyo 31/36 thread_localの効果 (revisited) thread_localの効果 ・グローバル変数をスレッドローカルにする ・暗黙のstatic指定がつく

    #pragma omp parallel for for(int i=0;i<thread_num;i++){ func(i); } void func(int thread_index){ std::vector<int> v; //... //... } malloc free thread_local指定無し→ 関数が呼ばれるとmalloc、抜ける時にfree thread_local指定 → std::vectorがstatic変数に(スコープが制限されたグローバル変数) → 最初の一度だけmalloc、プログラム終了時にfree → freeの回数が減る (チャンクリストのお片付け回数が減る) → 早くなる ※ flat-MPIの場合も毎回freeが走っていたが、チャンクリストが短かった?
  32. ISSP, Univ. of Tokyo 32/36 なぜハイブリッドではstd::vectorが遅くなるか? おそらくチャンクのリンクリストが長くなるのが原因 プロセス プロセス flat-MPI

    ハイブリッド プロセス 各プロセスが独自のアリーナを保持 プロセスあたりのメモリ空間が小さい 各アリーナのリンクリストも短く単純 複数のプロセスが一つのアリーナを共有 プロセスあたりのメモリ空間が大きい リンクリストが長く複雑に malloc_consolidateが遅いのが本質?
  33. 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されなくなる → デフラグが走らなくなることで高速化を期待
  34. 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通信のサイズ上限をゼロにして 強制的にランデブー通信指定
  35. 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で顕在化しなかった理由は、チャンクリストが短く単純だったから?
  36. 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実装は調べてないので不明