サブロウ丸

主にプログラミングと数学

master mind by c++; part15 MPIによるプロセス並列

今回やること

MPIのサンプルコードの紹介, 及び MPIによるプロセス並列の組み込み.

MPI

MPIとは, プロセス並列のための通信規格です. この規格を実際に実装しているものとして openMPI, MVAPICH, Intel MPI などのライブラリがあります. プロセス並列なので, それぞれのworker(並列単位)はお互いに異なるメモリ空間を持ち, データを共有するためにはメッセージパッシングを用いて行う必要があります.

通信規格( 関数名や引数 )は統一されているため, 用いるライブラリ(openMPI, MVAPICH, Intel MPI)を変えたとしても プログラムは変更する必要はありません. (ありがたい)

install

今回はopenMPIを使ってみます.

ubuntuであれば

$ apt install -y libopenmpi-dev

で入ります.

超簡単なサンプル実装

上記をsample_mpi.cppとして保存して,

$ mpicxx sample_mpi.cpp

コンパイルします. 実行は

$ mpirun -n 3 ./a.out
This worker is 2 of 3
This worker is 0 of 3
This worker is 1 of 3

3つのworkerがそれぞれ出力を行っていますね.

データ集約(MPI_Gather)の例

もう少し複雑な例を紹介します. それぞれのプロセスでデータを計算して, それをどこか一つのrankに集約する メッセージパッシングを書くとこうなります.

sbufは送信データを格納するバッファ, rbufは受信データを格納するバッファで rank = 0 がデータを受け取るので, rank = 0のみメモリ領域を確保するようにしています.

$ mpirun -n 4 ./a.out  
rank 0 send 0 1
rank 3 send 6 7
rank 2 send 4 5
rank 1 send 2 3
rbuf  0 1 2 3 4 5 6 7

rank = 0 のrbufにデータが集まっているのが分かりますね.

逐次的なデータ送信の例

次は少し複雑に次のような状況を実現してみます. f:id:inarizuuuushi:20210710134913p:plain

  • (rank > 0)はMPI_Isendで前回送信時から新しく行ったジョブ数をrank=0にtag=0で送信します. rank=0が受け取る前に送信バッファが変更されないようにdequeに送信する情報を格納してから送ります.
  • (rank = 0)はMPI_Recvでそれを受け取り, progress barを更新します.
  • (rank > 0)はMPI_Testで送信した情報をrank=0が受け取っているのかを確認します. 受け取り済みのものはdequeから削除します.
  • (rank > 0)は全てのジョブを行ったら, tag=1で0バイトのメッセージを送ります. ( 終了したことを伝えるためだけのもの )
  • (rank = 0)は全てのrank > 0 のworkerから終了メッセージを受け取ったのを確認してから終了します.

コンパイル

$ mpicxx mpi_progress.cpp -std=c++11 -I./source/progress-cpp/include

実行

$ mpirun -n 3 ./a.out 

MPIの組み込み

CMake

オプションを追加, オプションがONのときに

  • find_package(MPI)
  • CMAKE_CXX_COMPILER を MPI_CXX_COMPILER に変更 ( もっといい書き方があるかも )
option(USE_MPI "Use MPI" OFF)

if(USE_MPI)
  find_package(MPI REQUIRED)
  set(CMAKE_CXX_COMPILER ${MPI_CXX_COMPILER})  # Maybe there's a better way...
  message("Set CMAKE_CXX_COMPILER <-- ${CMAKE_CXX_COMPILER}")
endif()

Dockerfile

追加

RUN apt-get libopenmpi-dev

run.cpp

MPI並列用のtest関数. MPI_Gatherで, それぞれのrankのデータをrank=0に収集して, 統計情報を出力します. くわしはこちらを参照 ( 差分 )

void runTestPara(
      Config &config,
      int nProcesses,
      int rank
      ) noexcept
{
   CodePtrList S;
   allCodeGenerator(config, S);

   std::vector<int> countTable(S.size());
   std::vector<double> timeTable(S.size());

   // initialize the bar
   progresscpp::ProgressBar progressBar(S.size(), 70);

   for ( int i = 0; i*nProcesses + rank < static_cast<int>(S.size()); ++i )
   {
      if ( rank == 0 )
      {
         // 簡易的なprogress bar
         for ( int j = 0; j < nProcesses; j++ ) ++progressBar;
         progressBar.display();
      }

      // test code
      // (省略)

      assert( *testS[0] == *secret );
      double elapsed = end - start;
      countTable[i] = count;
      timeTable[i] = elapsed;
   }


   if( nProcesses > 0 )
   {
      // データ集め
      MPI_Gather(countTable.data(), sendcount, MPI_INT, countTable.data(), sendcount, MPI_INT, 0, MPI_COMM_WORLD);
      MPI_Gather(timeTable.data(), sendcount, MPI_DOUBLE, timeTable.data(), sendcount, MPI_DOUBLE, 0, MPI_COMM_WORLD);
   }

   if( rank == 0 )
   {
      auto endTotal = omp_get_wtime();
      // output statistics
      // (省略)
   }
}

Github-action (CMake)

CMake用のgithub actionに下記を追加. そのままだとCMakeの最中に

Could NOT find MPI (missing: MPI_CXX_FOUND)

なので, 単純に

sudo apt install -y openmpi-bin libopenmpi-dev

をCMake前に実行すればok.

    - name: Build OpenMPI
      run: sudo apt install -y openmpi-bin libopenmpi-dev

    - name: Configure CMake with MPI
      run: |
        rm -rf ${{github.workspace}}/build/*
        # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
        # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
        cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DUSE_MPI=ON

    - name: Build with MPI
      # Build your program with the given configuration
      run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}

    - name: Test with MPI
      working-directory: ${{github.workspace}}/build
      # Execute tests defined by the CMake configuration.
      # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail
      run: ctest -C ${{env.BUILD_TYPE}}

実験

$ OMP_NUM_THREADS=1 ./bin/mastermind ${num_colors} ${num_pins} --test --policy entropy

にて, 並列数ごとの total_timeを比較します. それぞれ10回実行して, 平均時間を算出します.

machine A

  • プロセッサ: 2.30GHz Intel(R) Xeon(R) CPU E5-2670 v3
num colors num pins total time[s]
processes=1
total time[s]
processes=4
total time[s]
processes=8
total time[s]
processes=16
4 4 0.458 0.179 0.107 0.064
5 4 3.009 1.027 0.565 0.330
6 4 24.444 6.876 3.937 2.089
3 5 0.331 0.136 0.085 0.052
4 5 14.098 4.014 2.317 1.308
5 5 203.150 55.562 31.100 15.789

こちらも順調に高速化されていますね.

まとめ

コード

参考

他の記事