我正在使用的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 worker等待并发队列中的新任务可用,然后使用类似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
}
我可以介绍一些睡眠功能,但这似乎是一个黑客,会降低我的吞吐量。对于这种饥饿/无效率问题,还有其他解决办法吗?
答案 0 :(得分:1)
我担心你可以通过循环检查来自本地线程和MPI_Testany
(或更好MPI_Testsome
)的新任务的最终解决方案做得最好。
你能做的一件事就是为此付出全部核心。优点是,这很简单,具有低延迟并提供可预测的性能。在现代HPC系统上,这通常是> 20个核心,所以&lt; 5%的开销。如果您的应用程序受内存限制,则开销甚至可以忽略不计。不幸的是,这浪费了CPU周期和能量。稍作修改就是在循环中引入usleep
。您必须调整睡眠时间以平衡利用率和延迟。
如果要为应用程序使用所有内核,则必须小心,以免MPI线程从计算线程中窃取CPU时间。我假设你的队列实现是阻塞的,即不忙等待。这导致这样的情况,即计算线程在等待时将CPU时间提供给MPI线程。不幸的是,发送这可能不是真的,因为工作人员可以在将任务放入队列后立即继续。
您可以做的是增加MPI线程的nice
级别(降低优先级),使其主要在计算线程等待结果时运行。您还可以在循环中使用sched_yield
为调度程序提供一些提示。虽然两者都在POSIX中定义,但它们的语义非常周,并且强烈依赖于actual scheduler implementation。使用sched_yield
实现繁忙的等待循环通常不是一个好主意,但您没有真正的替代方案。在某些情况下,OpenMPI和MPICH实现类似的循环。
额外MPI线程的影响取决于计算线程的紧密耦合程度。例如。如果它们经常处于障碍状态,它会严重降低性能,因为只需延迟一个线程就会延迟所有线程。
最后,如果您希望实施效率很高,则必须测量并调整到某个系统。
答案 1 :(得分:0)
我有一个避免忙碌等待的解决方案(有或没有睡觉),但它有自己的成本:你需要一个单独的MPI进程来帮助管理队列,和每个想要从多个线程发出请求的其他MPI进程必须能够通过其他一些IPC通道(例如套接字)与该进程通信。请注意,后一种限制(但我认为,并非完全)在一开始就消除了MPI的用处。基本的想法是多线程MPI幸福的主要障碍是,当其中一种口味是MPI时,不可能在两种不同口味的IPC上使用线程块,所以我们可以绕过它使用单独的MPI“转发器”进程将另一种形式的IPC请求“转换”为普通的MPI请求,并将其发送回原始进程,然后由该进程的“MPI监听器线程”接收并执行操作
您的MPI程序应包含以下进程和线程:
accept()
上保持无限循环阻塞。 (我将在这里使用套接字作为替代IPC机制的示例;其他的将以类似的方式工作。)在每个accept()
调用完成后,它从套接字读取编码请求,其中包含以下内容:请求进程的进程ID。然后立即对该进程ID进行(同步)MPI_Send()
,向其发送编码请求,并再次开始阻止accept()
。MPI_Waitany()
上保持无限循环阻塞,可以接收 2种不同类型的请求消息:
MPI_Send()
发布到目标MPI进程来处理在编码请求中识别。显然,转发器进程对请求的同步处理代表了系统中的一个瓶颈,但只需添加更多转发器进程就可以轻松扩展,这些进程的行为完全相同,并且工作进程选择哪个转发器进程“询问”随机
一种可能的优化方法是让转发器进程将“已转换”的请求直接发送到目标MPI进程,而不是返回到发起它的进程。