【问题标题】:Is it possible to use Catch2 for testing an MPI code?是否可以使用 Catch2 测试 MPI 代码?
【发布时间】:2020-02-05 22:47:52
【问题描述】:

我正在处理一个相当大的 MPI 代码。我开始将单元测试包含到现有的代码库中。但是,一旦被测单元使用 MPI 例程,测试可执行文件就会崩溃,并显示错误消息“在 MPI_Init 之前调用 MPI 例程”

  • 解决这个问题的最佳方法是什么?
  • 我可以运行具有多个 MPI 等级的测试吗?

【问题讨论】:

  • 我对 MPI 一无所知,但您尝试调用 MPI_Init 吗?
  • 是的,问题是如何以及在哪里正确地做到这一点。每次测试一次,或者以某种方式更聪明:D

标签: c++ mpi catch2


【解决方案1】:

当所有测试都通过后,我只需要听到一次(例如,按大师等级)。但我发现,如果测试失败,知道它在哪个等级以及如何失败仍然是有益的。我的解决方案不需要操作 catch.hpp 文件,而只需要自定义的 main 看起来像这样:

#define CATCH_CONFIG_RUNNER
#include "catch.hpp"
#include "mpiHelpers.hpp"
#include <sstream>

int main( int argc, char* argv[] ) {
    MPI_Init_H autoInit { argc, argv }; // calls MPI_Finalize() on destruction

    std::stringstream ss;

    /* save old buffer and redirect output to string stream */
    auto cout_buf = std::cout.rdbuf( ss.rdbuf() ); 

    int result = Catch::Session().run( argc, argv );

    /* reset buffer */
    std::cout.rdbuf( cout_buf );

    MPI_Comm_H world { MPI_COMM_WORLD };

    std::stringstream printRank;
    printRank << "Rank ";
    printRank.width(2);
    printRank << std::right << world.rank() << ":\n";

    for ( int i{1}; i<world.size(); ++i ){
        MPI_Barrier(world);
        if ( i == world.rank() ){
            /* if all tests are passed, it's enough if we hear that from 
             * the master. Otherwise, print results */
            if ( ss.str().rfind("All tests passed") == std::string::npos )
                std::cout << printRank.str() + ss.str();
        }
    }
    /* have master print last, because it's the one with the most assertions */
    MPI_Barrier(world);
    if ( world.isMaster() )
        std::cout << printRank.str() + ss.str();

    return result;
}

所以我只是将输出重定向到字符串流缓冲区。然后我可以稍后决定是否打印它们。

MPI_Init_HMPI_Comm_H 这两个辅助类并不是必需的,您可以使用标准的 MPI_InitMPI_Comm 来完成,但为了完整起见,这里是:

#ifndef MPI_HELPERS
#define MPI_HELPERS

#include <iostream>
#include <mpi.h>

class MPI_Init_H {
    public:
    /* constructor initialises MPI */
    MPI_Init_H( int argc, char* argv[] ){
        MPI_Init(&argc, &argv);
    }
    /* destructor finalises MPI */
    ~MPI_Init_H(){ MPI_Finalize(); }
};

/* content of mpiH_Comm.hpp */
class MPI_Comm_H
{
private:
    MPI_Comm m_comm;
    int m_size;
    int m_rank;

public:
    MPI_Comm_H( MPI_Comm comm = MPI_COMM_NULL ) :
        m_comm {comm}
    {
        if ( m_comm != MPI_COMM_NULL ){
            MPI_Comm_size(m_comm, &m_size);
            MPI_Comm_rank(m_comm, &m_rank);
        }
        else {
            m_size = 0;
            m_rank = -1;
        }
    }

    /* contextual conversion to bool, which returns true if m_comm is a valid
     * communicator and false if it is MPI_COMM_NULL */
    operator bool() const { return m_comm != MPI_COMM_NULL; }
    
    const MPI_Comm& comm() const {
        #ifndef NDEBUG
        if ( !(*this) )
            std::cerr
                << "WARNING: You called comm() on a null communicator!\n";
        #endif
        return m_comm;
    }

    int rank() const {
        return m_rank;
    }

    int size() const {
        assert( *this && "You called size() on a null communicator!");
        return m_size;
    }

    int master() const {
        assert( *this && "You called master() on a null communicator!");
        return 0;
    }

    bool isMaster() const { return rank() == 0; }

    /* allow conversion to MPI_Comm */
    operator const MPI_Comm&() const { return m_comm; }
};

#endif

【讨论】:

    【解决方案2】:

    除了提供自定义 main 函数外,还可以通过强制每个 MPI 进程将其测试报告转储到单独的文件中来规避经常遇到的来自多个 MPI 进程的重复输出问题,例如进程p 将其测试报告转储到report_p.xml

    一种快速而肮脏的方法是扩展命令行参数向量argv,为进程相关的文件名加上--out 配对的额外条目。

    来源:

    // mytests.cpp
    #define CATCH_CONFIG_RUNNER
    
    #include "catch.hpp"
    #include <mpi.h>
    #include <string>
    #include <vector>
    
    int 
    main(int argc, char* argv[]) {
      MPI_Init(&argc, &argv);
    
      int mpi_rank;
      MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank);
    
      // make space for two extra arguments
      std::vector<const char*> new_argv(argc + 2);
      for (int i = 0; i < argc; i++) {
        new_argv[i] = argv[i];
      }
    
      // set "--out report_p.xml" as last two arguments
      auto filename = "report_" + std::to_string(mpi_rank) + ".xml";
      new_argv[argc] = "--out";
      new_argv[argc+1] = filename.data();
    
      int result = Catch::Session().run(new_argv.size(), new_argv.data());
      MPI_Finalize();
      return result;
    }
    

    编译并运行:

    mpic++ mytests.cpp -o mytests
    mpirun -np 4 ./mytests --reporter junit
    

    预期的输出文件

    report_0.xml
    report_1.xml
    report_2.xml
    report_3.xml
    

    使用std::ostream 写入同一文件的 MPI 进程存在破坏 XML 格式、使 JUnit XML 解析器崩溃和 CI 管道失败的风险,即使所有测试都通过了。将每个进程的测试报告转储到它们自己的单独文件中可以避免这个问题。如果需要,可以稍后连接单独的文件。

    即使上面示例中的额外参数已放置在参数列表的末尾,它们也不会覆盖命令行中任何先前的 --out 键值。相反,内置 CLI 解析器将其视为参数关键字的重复并引发错误。因此,上述方法的更具包容性的实现不会扩展参数列表,而是将与 --out 的值对应的 argv 指针替换为附加的文件名。

    PS:pwaulAnti Earth 一定对 Catch 源代码进行了认真的挖掘,以找出智能代码注入,向他们致敬!

    【讨论】:

      【解决方案3】:

      对于希望在运行 Catch2 分布式时删除所有重复的控制台输出的任何人,这里有一个解决方案。

      catch.hpp 中找到ConsoleReporter 的定义(在v2.10.0 中,它位于第15896 行)。它看起来像:

      ConsoleReporter::ConsoleReporter(ReporterConfig const& config)
          : StreamingReporterBase(config),
          m_tablePrinter(new TablePrinter(config.stream(),
              [&config]() -> std::vector<ColumnInfo> {
              if (config.fullConfig()->benchmarkNoAnalysis())
              {
                  return{
                      { "benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 43, ColumnInfo::Left },
                      { "     samples", 14, ColumnInfo::Right },
                      { "  iterations", 14, ColumnInfo::Right },
                      { "        mean", 14, ColumnInfo::Right }
                  };
              }
              else
              {
                  return{
                      { "benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 32, ColumnInfo::Left },
                      { "samples      mean       std dev", 14, ColumnInfo::Right },
                      { "iterations   low mean   low std dev", 14, ColumnInfo::Right },
                      { "estimated    high mean  high std dev", 14, ColumnInfo::Right }
                  };
              }
          }())) {}
      ConsoleReporter::~ConsoleReporter() = default;
      

      虽然此处未显示,但基类 StreamingReporterBase 提供了一个 stream 属性,我们将通过 failbit 显示的技巧 here 禁用该属性。

      在上面最后的{}(一个空的构造函数定义)内,插入:

      // I solemnly swear that I am up to no good
      int rank;
      MPI_Comm_rank(MPI_COMM_WORLD, &rank);
      
      // silence non-root nodes
      if (rank != 0)
          stream.setstate(std::ios_base::failbit);  
      

      您可以在this repo 上查看示例。

      【讨论】:

        【解决方案4】:

        是的,有可能。

        https://github.com/catchorg/Catch2/issues/566 中所述,您必须提供自定义主函数。

        #define CATCH_CONFIG_RUNNER
        #include "catch.hpp"
        #include <mpi.h>
        
        int main( int argc, char* argv[] ) {
            MPI_Init(&argc, &argv);
            int result = Catch::Session().run( argc, argv );
            MPI_Finalize();
            return result;
        }
        

        为了增强您将 Catch2 与 MPI 结合使用的体验,您可能希望避免多余的控制台输出。这需要将一些代码注入到 catch.hpp 的 ConsoleReporter::testRunEnded 中。

        #include <mpi.h>
        void ConsoleReporter::testRunEnded(TestRunStats const& _testRunStats) {
            int rank id = -1;
            MPI Comm rank(MPI COMM WORLD,&rank id);
            if(rank id != 0 && testRunStats.totals.testCases.allPassed())
                return;
            printTotalsDivider(_testRunStats.totals);
            printTotals(_testRunStats.totals);
            stream << std::endl;
            StreamingReporterBase::testRunEnded(_testRunStats);
        }
        

        最后,您可能还希望使用不同数量的 MPI 等级来执行您的测试用例。我发现以下是一个简单且运行良好的解决方案:

        SCENARIO("Sequential Testing", "[1rank]") {
            // Perform sequential tests here
        }
        SCENARIO("Parallel Testing", "[2ranks]") {
            // Perform parallel tests here
        }
        

        然后你可以单独调用标签场景

        mpiexec -1 ./application [1rank]
        mpiexec -2 ./application [2rank]
        

        【讨论】:

        • 为了抑制重复输出,必须将这个return 添加到所有写入streamConsoleReporter 方法中(还有很多!)。请参阅下面的答案以停止所有输出。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-01-06
        • 1970-01-01
        • 1970-01-01
        • 2015-01-01
        • 2014-02-24
        • 2015-01-07
        • 2020-02-21
        相关资源
        最近更新 更多