驾驭3200Gbps网络(12): 绑定CPU核心
在上一章中,我们启用了多线程,为每一张显卡创建了一个线程。然而传输速度依然仅为 355.301 Gbps,只达到了总带宽 3200 Gbps 的 11.1%。
如果我们仔细对比上一章程序的运行过程与再上一章单线程的版本的运行过程,我们会发现一个问题:生成随机数这一步变慢了。尽管生成随机数的用时并不被计入传输速度的测试,但是这一现象本身仍然十分奇怪。尽管使用了多个线程,但是我们仍然只使用一个线程来生成随机数,那么为什么生成随机数的速度会变慢呢?
一个可能的原因是操作系统在多个线程之间切换的时候,会把线程切换到不同的 CPU 核心上。这样一来,线程在生成随机数的时候,会在不同的 CPU 缓存之间来回切换,导致了缓存的失效。甚至有可能,线程在生成随机数的时候,会在不同的 NUMA 节点之间来回切换,导致了内存的访问延迟,以及导致了 GPU 显存的访问延迟。
既然这一问题会使得随机数生成变慢,那么同样也很可能使网络完成队列的处理以及新操作的提交变慢。为了解决这一问题,我们可以尝试将每个线程绑定到一个 CPU 核心上。这样一来,线程在生成随机数以及进行网络传输时,就不会在不同的 CPU 核心之间来回切换,也不会在不同的 NUMA 节点之间来回切换。我们把本章的程序命名为 12_pin.cpp
。
绑定CPU核心
在第八章的总线拓扑探测中,我们已经把所有的 CPU 物理核心按照 NUMA 平均分配给了8张显卡。这里我们只需要对于每张显卡任意挑出一个 CPU 核心,然后把显卡对应的线程绑定到这个 CPU 核心上即可。绑定 CPU 核心可以通过 pthread_setaffinity_np()
函数来实现。
int ServerMain(int argc, char **argv) {
// ...
// Multi-thread Poll completions
std::vector<std::thread> threads;
threads.reserve(num_gpus);
for (size_t gpu_idx = 0; gpu_idx < net_groups.size(); ++gpu_idx) {
const auto &cpus = topo_groups[gpu_idx].cpus;
int preferred_cpu = cpus[cpus.size() / 2];
threads.emplace_back([&s, preferred_cpu, gpu_idx] {
// Pin CPU
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(preferred_cpu, &cpuset);
CHECK(pthread_setaffinity_np(
pthread_self(), sizeof(cpu_set_t), &cpuset) == 0);
// Poll completions
// ...
});
}
// ...
}
运行效果
从上面的视频中可以看到,我们的程序绑定了 CPU 核心之后,传输速度达到了 1237.738 Gbps,达到了总带宽 3200 Gbps 的 38.7%。这是一次不错的提升,不过离我们的目标还有一定的距离,我们还需要进一步优化。
本章代码:https://github.com/abcdabcd987/libfabric-efa-demo/blob/master/src/12_pin.cpp