有效地收集/分散任务

Posted

技术标签:

【中文标题】有效地收集/分散任务【英文标题】:Efficiently gathering/scattering tasks 【发布时间】:2016-10-21 07:11:01 【问题描述】:

我正在使用的 MPI 实现本身并不支持完整的多线程操作(***别是 MPI_THREAD_SERIALIZED,原因很复杂),所以我试图将来自多个线程的请求汇集到单个工作线程中,然后将结果分散回多个线程。

我可以通过使用并发队列轻松地处理收集本地请求任务,并且 MPI 本身支持排队异步任务。然而,问题在于让双方互相交谈:

为了将响应分散回各个线程,我需要在当前进行中的请求上调用 MPI_Waitany 之类的东西,但在此期间 MPI 工作人员被有效阻止,因此它无法收集和提交本地工人的任何新任务。

// mpi worker thread
std::vector<MPI_Request> requests; // in-flight requests
while(keep_running)

    if(queue.has_tasks_available())
    
        MPI_Request r;
        // task only performs asynchronous MPI calls, puts result in r
        queue.pop_and_run(task, &r);
        requests.push_back(r);
    
    int idx;
    MPI_Waitany(requests.size(), requests.data(), &idx,
                MPI_STATUS_IGNORE); // problems here! can't get any new tasks
    dispatch_mpi_result(idx); // notifies other task that it's response is ready
    // ... erase the freed MPI_Request from requests

同样,如果我只是让 mpi 工作人员等待新任务从并发队列中可用,然后使用类似 MPI_Testany 的方式轮询 MPI 响应,那么充其量响应可能需要很长时间才能完成实际上是到本地工作人员那里,最坏的情况是 mpi 工作人员会死锁,因为它正在等待本地任务,但所有任务都在等待 mpi 响应。

// mpi worker thread
std::vector<MPI_Request> requests; // in-flight requests
while(keep_running)

    queue.wait_for_available_task(); // problem here! might deadlock here if no new tasks happen to be submitted
    MPI_Request r;
    queue.pop_and_run(task, &r);
    requests.push_back(r);
    int idx;
    MPI_Testany(requests.size(), requests.data(), &idx, MPI_STATUS_IGNORE);
    dispatch_mpi_result(idx); // notifies other task that its response is ready
    // ... erase the freed MPI_Request from requests

我能看到解决这两个问题的唯一解决方案是让 mpi 工作人员只轮询双方,但这意味着我有一个永久固定的线程来处理请求:

// mpi worker thread
std::vector<MPI_Request> requests; // in-flight requests
while(keep_running)

    if(queue.has_tasks_available())
    
        MPI_Request r;
        // task only performs asynchronous MPI calls, puts result in r
        queue.pop_and_run(task, &r);
        requests.push_back(r);
    
    int idx;
    MPI_Testany(requests.size(), requests.data(), &idx, MPI_STATUS_IGNORE);
    dispatch_mpi_result(idx); // notifies other task that its response is ready
    // ... erase the freed MPI_Request from requests

我可以引入某种睡眠功能,但这似乎是一种 hack,会降低我的吞吐量。对于这种饥饿/效率低下的问题,还有其他解决方案吗?

【问题讨论】:

恐怕您的分析完全正确。许多 MPI 实现在等待消息时使用 100% CPU。您的问题不仅需要有效地阻塞消息,还需要同时阻塞本地队列。为了完整起见,最好知道您正在运行哪个 MPI 实现和哪个操作系统(内核版本)。在调度程序的帮助下,您可能会做得更好。 我正在设计我的代码以在各种平台和 MPI 实现中运行,但我相信遇到此问题的主要实现是 OpenMPI。我知道有一个标志可以启用MPI_THREAD_MULTIPLE,但在我测试过的系统上,通常需要自己构建 OpenMPI,而且据说不使用 Infiniband。该代码旨在支持工作站到集群规模的系统,所以我不知道关于操作系统的任何假设是否太有用,除了它有点现代(过去 5 年或更新)、基于 Unix 并且具有 pthreads。跨度> 【参考方案1】:

恐怕你已经接近你能做的最好的了,你的最终解决方案是循环检查来自本地线程和MPI_Testany(或更好的MPI_Testsome)的新任务。

您可以做的一件事就是为此投入整个核心。优点是,这很简单,具有低延迟并提供可预测的性能。在现代 HPC 系统上,这通常是 > 20 个内核,因此开销小于 5%。如果您的应用程序受内存限制,则开销甚至可以忽略不计。不幸的是,这会浪费 CPU 周期和能量。一个小的修改是在循环中引入usleep。您必须调整睡眠时间以平衡利用率和延迟。

如果您想为应用程序使用所有内核,则必须小心,以免 MPI 线程从计算线程中窃取 CPU 时间。我假设您的队列实现正在阻塞,即不忙于等待。这导致了这样一种情况,即计算线程可以在等待时为 MPI 线程提供 CPU 时间。不幸的是,发送这可能不是真的,因为工作人员可以在将任务放入队列后立即继续。

您可以做的是提高 MPI 线程的nice 级别(降低优先级),以便它主要在计算线程等待结果时运行。您还可以在循环中使用sched_yield 给调度程序一些提示。虽然两者都是在 POSIX 中定义的,但它们的语义非常周到并且强烈依赖于actual scheduler implementation。使用sched_yield 实现繁忙的等待循环通常不是一个好主意,但是您没有真正的选择。 OpenMPI 和 MPICH 在某些情况下实现了类似的循环。

额外 MPI 线程的影响取决于计算线程的耦合程度。例如。如果它们经常处于障碍中,则会严重降低性能,因为仅延迟单个线程就会延迟所有线程。

最后,如果您希望实现高效,则必须针对某个系统进行衡量和调整。

【讨论】:

【参考方案2】:

我有一个解决方案可以避免忙等待(无论是否有睡眠),但它有其自身的成本:您需要一个单独的 MPI 进程来帮助管理队列,每个其他想要从多个线程发出请求的 MPI 进程必须能够通过其他 IPC 通道(例如套接字)与该进程通信。请注意,后一种限制在某种程度上(但我认为,并非完全)首先排除了 MPI 的用处。基本思想是,多线程 MPI 幸福的主要障碍是当其中一种是 MPI 时,不可能在两种不同风格的 IPC 中的 任何一种上拥有线程块,因此我们可以通过使用单独的 MPI“转发器”进程将另一种形式的 IPC 请求“转换”为普通 MPI 请求,并将其发送回原始进程,由该进程的“MPI 侦听器线程”接收并执行.

您的 MPI 程序应包含以下进程和线程:

    一个特殊的“转发器”进程,它只有一个线程,在accept() 上处于无限循环阻塞状态。 (我将在这里使用套接字作为替代 IPC 机制的示例;其他的将以类似的方式工作。)在每个 accept() 调用完成后,它会从套接字读取一个编码请求,其中包含进程 ID 等内容的请求过程。然后它立即向该进程 ID 发出(同步)MPI_Send(),向它发送编码请求,并再次开始阻塞 accept()。 任何数量的其他进程,每个进程都有: 一个“MPI 侦听器”线程,在MPI_Waitany() 上处于无限循环阻塞状态,可以接收2 种不同类型的请求消息
      来自其他进程的普通“传入”请求以执行某项操作,这些请求应该按照您当前的处理方式进行处理,并且 来自“转发器”进程的请求表示由在同一进程中的其他线程发起的“传出”请求,应通过将异步MPI_Send() 发送到中标识的目标 MPI 进程来处理编码的请求。
    任意数量的工作线程,只要它们需要发出请求,就会与转发器进程建立套接字连接,传输编码的请求,然后关闭套接字。

显然,转发器进程对请求的同步处理是系统中的一个瓶颈,但只需添加更多行为完全相同的转发器进程并让工作进程选择“询问”哪个转发器进程,就可以轻松扩展随机。

一种可能的优化是让转发器进程将“转换后的”请求直接发送到目标 MPI 进程,而不是返回到发起它的进程。

【讨论】:

以上是关于有效地收集/分散任务的主要内容,如果未能解决你的问题,请参考以下文章

快速搭建Jenkins集群

快速搭建Jenkins集群

敏捷估算与规划—规划失败的原因

在 pandas 中有效地使用替换

Flink on yarn 实时日志收集最佳实践

Puppet学习之cron任务的管理