我正在使用的 MPI 实现本身并不支持完整的多线程操作(最高级别是 MPI_THREAD_SERIALIZED
,原因很复杂),所以我试图将来自多个线程的请求汇集到一个单个工作线程,然后将结果分散回多个线程。
通过使用并发队列,我可以轻松地处理收集本地请求任务,并且 MPI native 支持排队异步任务。然而,问题是让双方相互交谈:
为了将响应分散回各个线程,我需要对当前进行中的请求调用类似 MPI_Waitany
的方法,但在此期间 MPI worker 被有效阻塞,因此它无法从本地工作人员那里收集和提交任何新任务。
// 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 worker 等待新任务从并发队列中可用,然后使用类似 MPI_Testany
的方式轮询 MPI 响应,那么最好的响应可能要么实际到达本地 worker 需要很长时间,最坏的情况是 mpi worker 会死锁,因为它正在等待本地任务,但所有任务都在等待 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 worker 只轮询双方,但这意味着我有一个永久 Hook 的线程来处理请求:
// 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_Testany
(或更好的MPI_Testsome
)的新任务>).
您可以做的一件事就是为此投入整个核心。优点是,这很简单,具有低延迟并提供可预测的性能。在现代 HPC 系统上,这通常是 > 20 个内核,因此开销 < 5%。如果您的应用程序受内存限制,则开销甚至可以忽略不计。不幸的是,这会浪费 CPU 周期和能源。一个小的修改是在循环中引入 usleep
。您必须调整 sleep 时间以平衡利用率和延迟。
如果你想为应用程序使用所有内核,你必须小心 MPI 线程不会从计算线程中窃取 CPU 时间。我假设你的队列实现是阻塞的,即不忙等待。这导致了计算线程在等待时可以给 CPU 时间给 MPI 线程的情况。不幸的是,发送这可能不是真的,因为工作人员可以在将任务放入队列后立即继续。
您可以做的是增加 MPI 线程的 nice
级别(降低优先级),以便它主要在计算线程等待结果时运行。您还可以在循环中使用 sched_yield
向调度程序提供一些提示。虽然两者都是在 POSIX 中定义的,但它们的语义非常弱并且强烈依赖于 actual scheduler implementation .用 sched_yield
实现繁忙的等待循环通常不是一个好主意,但您别无选择。 OpenMPI 和 MPICH 在某些情况下实现了类似的循环。
额外的 MPI 线程的影响取决于您的计算线程的紧密耦合程度。例如。如果它们经常处于障碍中,则会严重降低性能,因为仅延迟单个线程就会延迟所有线程。
最后,如果您希望实现高效,则必须针对特定系统进行测量和调整。
关于c++ - 有效地收集/分散任务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37915579/