背景和目的
为了更好的监测GPU集合通信时的网络情况,我们期望能够在NCCL里添加细粒度的GPU间点对点流量测量。在nccl中,GPU之间的通信可以理解为多次send/recv操作,每次操作绑定到具体的网卡(NIC)上执行,具体的数据传输流程由多对QP完成。在其中,每次QP任务的下发会由app生成WR(work request)告知具体的网卡,因此,我们期望能够统计端口上的WR数量,通过WR的变化趋势得知工作队列的长度变化,进而推测出当前网络的拥塞情况。此外,我们同时统计了端口上工作队列中剩余未发送的数据大小,并根据wc(work complete)的产生精确计算出了目前的GPU间点对点流量带宽。
设计与实现
wr端口数量及剩余未发送数据大小统计
对每个proxy线程,设置thread_local变量数组sendWrCounter与remainWrDataSize,NCCL_IB_MAX_DEVS_PER_NIC大小为网卡上最大端口数量
// count the number of send wrs and remain data sizes in wrs
thread_local int sendWrCounter[NCCL_IB_MAX_DEVS_PER_NIC] = {0};
thread_local int remainWrDataSize[NCCL_IB_MAX_DEVS_PER_NIC] = {0};
在数据send过程中,每个req对应一个wr,nccl将每个wr以链式结构连接,并在最后添加一个lastwr以获得wc(只有lastwr完成才会产生wc,wc的产生意味着所有串接的wr均已完成)。
在发送过程中,每个req会用多个qp进行分割,每个qp对应着固定的网卡端口,因此我们可以准确得知每个req对应的需要在不同端口上发送的数据大小。同时,为了便于统计以及观察wr数量变化,我们假设同一个wr经过n个qp划分后会产生(n+1)个wr,因此,我们依据该逻辑在数据send时增加wrCounter以及wrDataSize。
ncclResult_t ncclIbMultiSend(struct ncclIbSendComm* comm, int slot) {
......
for (int i = 0; i < nqps; i++) {
int qpIndex = comm->base.qpIndex;
ncclIbQp* qp = comm->base.qps + qpIndex;
int devIndex = qp->devIndex;
for (int r=0; r<nreqs; r++) {
......
// update sendWrCounter
sendWrCounter[devIndex]++;
......
int chunkSize = DIVUP(DIVUP(reqs[r]->send.size, nqps), align) * align;
int length = std::min(reqs[r]->send.size-reqs[r]->send.offset, chunkSize);
//update remainWrDataSize
if (length > 0) {
remainWrDataSize[devIndex] += length;
}
......
}
}
}
在ncclIbItest中,会检测对应req是否已经获取wc,我们可以在收到对应的wc以后,同步减少端口上的wrCounter以及wrDataSize。
ncclResult_t ncclIbTest(void* request, int* done, int* sizes) {
......
while (1) {
if (r->events[0] == 0 && r->events[1] == 0) {
......
}
int totalWrDone = 0;
int wrDone = 0;
struct ibv_wc wcs[4];
for (int i = 0; i < NCCL_IB_MAX_DEVS_PER_NIC; i++) {
TIME_START(3);
// If we expect any completions from this device's CQ
if (r->events[i]) {
NCCLCHECK(wrap_ibv_poll_cq(r->devBases[i]->cq, 4, wcs, &wrDone));
totalWrDone += wrDone;
if (wrDone == 0) { TIME_CANCEL(3); } else { TIME_STOP(3); }
if (wrDone == 0) continue;
for (int w=0; w<wrDone; w++) {
struct ibv_wc *wc = wcs+w;
if (wc->status != IBV_WC_SUCCESS) {
......
}
......
if (req->type == NCCL_NET_IB_REQ_SEND) {
// update sendWrCounter
sendWrCounter[i] -= req->nreqs;
for (int j = 0; j < req->nreqs; j++) {
struct ncclIbRequest* sendReq = r->base->reqs+((wc->wr_id >> (j*8)) & 0xff);
if ((sendReq->events[i] <= 0)) {
WARN("NET/IB: sendReq(%p)->events={%d,%d}, i=%d, j=%d <= 0", sendReq, sendReq->events[0], sendReq->events[1], i, j);
ret = ncclInternalError;
goto ret;
}
sendReq->events[i]--;
if(global_timer_log.collect && sendReq->log[i].loged_start == NCCL_LOG_TELEMETRY && !sendReq->events[i]){
clock_gettime(CLOCK_REALTIME, &sendReq->log[i].send_end);
sendReq->log[i].diff = 1000000000L * (sendReq->log[i].send_end.tv_sec) + sendReq->log[i].send_end.tv_nsec;
sendReq->log[i].sendWrCounter = sendWrCounter[i];
sendReq->log[i].devIndex = i;
//sendReq has completed transport, update remainWrDataSize
remainWrDataSize[i] -= sendReq->log[i].size;
sendReq->log[i].remainWrDataSize = remainWrDataSize[i];
if((sendReq->log[i].func == 2 || sendReq->log[i].func == 3) && sendReq->log[i].size > 16){
// save log
pthread_mutex_lock(&global_timer_log.lock);
__sync_synchronize();
global_timer_log.push(sendReq->log[i]);
pthread_mutex_unlock(&global_timer_log.lock);
}
}
}
} else {
......
}
}
}
}
}
随后利用timerLogService进行异步输出到log文件中,输出的格式如下:
"%s: Group:%lu, from rank %d to rank %d, devIndex: %d, channel_id:%d, func:%u,FuncTimes:%lld, %d.%d.%d.%d->%d.%d.%d.%d, bandWidths: %d Gbps, sendWrcounter: %d, remainWrDataSize: %d Bytes, timestamp: %lld nsec"
流量带宽计算
在每次调用ncclIbItest时,若req->events[i]清零,则意味着req对应数据已经传送完成,我们记录当前时间戳以及发送完成的数据,将其存入log存入队列中,在timerLogService中,我们取出队列中的元素,并维护一个滑动窗口来统计实际的带宽(由于每次log间的时间间隔内获得的size,只能意味着该req在该时间间隔内完成,并不意味着整个req传输只花费了该时间间隔,同时也不意味着该时间间隔只用来发送该req对应数据)。我们设置滑动窗口大小(MAXWINDOWSIZE,通常设置为50),该滑动窗口为一组log的集合,我们认为该滑动窗口内所发送完成的数据,以及该窗口的时间间隔之比,是较为精确的流量带宽计算,同时,每次由新log到来时,我们将窗口右移,能够在常数时间内计算出每一时刻的流量带宽。
int getBandWidths() {
if (slideWindow.size() <= 1) {
return 0;
}
// Gbps
unsigned long long sendTime = sendEndTime - slideWindow.front().diff;
unsigned long long sendDataSizes = windowDataSizes - slideWindow.front().size;
// 0.93 = 1e9 / 1024 * 1024 * 1024
return sendDataSizes * 0.93 * 8 / sendTime;
}
测试结果
测试环境
西云87、西云94两台单机八卡服务器,需要利用VPN连接
跳板机:<http://100.64.27.220>
用户名:westhpc
密码:Tywl@123
100.64.24.87
100.64.24.94
用户名:root
密码:Tywl@123
测试流程
- 通过ssh连接服务器
- 进入docker环境,可能的指令为:
docker exec -it test bash - 在**/work/infrawaves/nccl-test**目录下,执行以下命令,进行4打4的all_gather测试,数据大小8G
mpirun -np 8 --host 100.64.24.87:4,100.64.24.94:4 --allow-run-as-root -mca btl ^openib -x NCCL_IB_GID_INDEX=3 -x NCCL_IB_TC=160 -x NCCL_NET_GDR_LEVEL=4 -x LD_LIBRARY_PATH=/workspace/infrawaves/zhangmj/vccl_2.21.51x/build/lib/ -x NCCL_TELEMETRY_ENABLE=1 -x NCCL_TELEMETRY_LOG_PATH=/workspace/infrawaves/Train/logs/ ./build/all_gather_perf -b 8G -e 8G -f 2 -g 1 -w 0
LD_LIBRARY_PATH可替换为你自己的编译lib路径
- 测试结果类似如下:

其中包含网卡名称、rank、端口号、channel Id、带宽等等信息。