NCCL源码详解3:通信器初始化ncclCommInitRank() 含视频教程

上上节课我们Nvidia NCCL的官方案例中

NCCL源码解读1:NCCL使用/调用案例 Example : One Device per Process or Thread-CSDN博客

已经讲了,第四步:4、通信初始化如何获取唯一ID UniqueId

NCCL源码详解2:通信初始化如何获取唯一ID UniqueId,ncclGetUniqueId()中ncclInit()、bootstrapGetUniqueId()包含视频教程-CSDN博客

今天讲,第六步是:6、初始化NCCL通信器。

初始化NCCL通信器的源码ncclCommInitRank()

一、ncclCommInitRank()源码详解

 哈哈哈,视频教程在这

哔哩哔哩_bilibili

什么是通信器?

通信器:NCCL通信器是一个用于管理多GPU环境中进程间通信的集合,通过定义通信域来支持并行计算任务的执行。一旦通信器被创建,进程就可以使用这个通信器来进行集合通信操作/原语(Broadcast、AllReduce等),例如数据同步和数据传输等。

简单点说:每个GPU知道我要和哪些GPU通信,并且能够进行集合通信。

哈哈哈,通信器的定义是我瞎编的,官网没找到定义。

ncclCommInitRank()核心逻辑

1、 加载CUDA驱动

2、获取当前CUDA设备ID

3、根据rank号和CUDA设备ID等,完成NCCL通信器初始化

源码速递

源码位置:nccl-master\src\init.cc

ncclResult_t ncclCommInitRank(ncclComm_t* newcomm, int nranks, ncclUniqueId commId, int myrank) {  

  // 1、加载CUDA驱动/  
  (void)ncclCudaLibraryInit();  
  
  // 声明一个变量来存储当前CUDA设备的ID  
  int cudaDev;  

  // 初始化NCCL配置结构体,使用默认的配置  
  ncclConfig_t config = NCCL_CONFIG_INITIALIZER;  

  // 2、获取当前CUDA设备ID   
  CUDACHECK(cudaGetDevice(&cudaDev));  
  
  // 存储参数方便可视化和分析 
  NvtxParamsCommInitRank payload{myrank, nranks, cudaDev};  
  NVTX3_FUNC_WITH_PARAMS(CommInitRank, CommInitRankSchema, payload)  

  //3、根据rank号和CUDA设备ID等,完成NCCL通信器初始化/
  // 调用ncclCommInitRankDev函数来初始化NCCL通信器  
  // ncclCommInitRankDev是一个更底层的函数,它允许指定CUDA设备ID和NCCL配置    
  NCCLCHECK(ncclCommInitRankDev(newcomm, nranks, commId, myrank, cudaDev, &config));  
  
  // 如果所有操作都成功完成,则返回ncclSuccess表示成功  
  return ncclSuccess;  

}

ncclCommInitRank()中最最核心的显然是第3步ncclCommInitRankDev(),下面我们向下挖一挖。

二、ncclCommInitRankDev()源码解读

ncclCommInitRankDev()核心逻辑

1、确保NCCL驱动已经初始化

2、确保CUDA驱动已经初始化

3、检查参数/指针是否为空,是否在有效范围内

4、配置NCCL通信器的一些属性,是否阻塞,通信通道数量等

5、分配一个作业对象 job,并设置作业对象的各个成员变量

6、使用 ncclAsyncLaunch 异步启动 ncclCommInitRankFunc 函数来初始化通信。 异步启动通信器初始化

7、退出和错误处理

源码速递

源码位置:nccl-master\src\init.cc

static ncclResult_t ncclCommInitRankDev(ncclComm_t* newcomm, int nranks, ncclUniqueId commId, int myrank, int cudaDev, ncclConfig_t *config) {
  ncclResult_t res = ncclSuccess; // 初始化结果,成功则为ncclSuccess
  ncclComm_t comm = NULL; // 初始化通信对象指针为NULL
  struct ncclCommInitRankAsyncJob *job = NULL; // 初始化异步任务结构体指针为NULL
  const char* env = ncclGetEnv("NCCL_COMM_ID"); // 获取环境变量NCCL_COMM_ID的值

  if (env && myrank == 0) { // 如果环境变量存在且当前节点是第一个节点
    INFO(NCCL_ENV, "NCCL_COMM_ID set by environment to %s", env); // 输出日志,显示环境变量设置的值
    NCCLCHECKGOTO(bootstrapCreateRoot((struct ncclBootstrapHandle*)&commId, true), res, fail); // 创建根节点
  }

  1、确保NCCL驱动已经初始化
  NCCLCHECKGOTO(ncclInit(), res, fail); // 初始化NCCL库
  if (myrank == 0) showVersion(); // 如果当前节点是第一个节点,显示版本信息

  2、确保CUDA运行时已经初始化
  CUDACHECKGOTO(cudaFree(NULL), res, fail); // 尝试释放NULL指针,如果失败则返回错误

  3、检查参数/指针是否为空,是否在有效范围内
  NCCLCHECKGOTO(PtrCheck(newcomm, "CommInitRank", "newcomm"), res, fail); // 检查通信对象指针是否有效
  NCCLCHECKGOTO(PtrCheck(config, "CommInitRank", "config"), res, fail); // 检查配置结构体指针是否有效
  if (nranks < 1 || myrank < 0 || myrank >= nranks) { // 验证输入参数
    WARN("Invalid rank requested : %d/%d", myrank, nranks); // 如果参数无效,输出警告
    res = ncclInvalidArgument; // 设置结果为无效参数错误
    goto fail; // 跳转到失败处理部分
  }

  3、配置NCCL通信器的一些属性,是否阻塞,通信通道数量等
  NCCLCHECKGOTO(ncclCalloc(&comm, 1), res, fail); // 为通信对象分配内存
  comm->startMagic = comm->endMagic = NCCL_MAGIC; // 设置通信对象的开始和结束魔术数,用于检测通信对象损坏
  NCCLCHECKGOTO(ncclCudaHostCalloc((uint32_t**)&comm->abortFlag, 1), res, fail); // 为通信对象的中断标志分配内存
  NCCLCHECKGOTO(ncclCalloc((uint32_t**)&comm->abortFlagRefCount, 1), res, fail); // 为通信对象的中断标志引用计数分配内存
  *comm->abortFlagRefCount = 1; // 初始化中断标志引用计数为1
  NCCLCHECKGOTO(parseCommConfig(comm, config), res, fail); // 解析通信配置
  /* start with ncclInternalError and will be changed to ncclSuccess if init succeeds. */
  comm->initState = ncclInternalError; // 初始化通信对象的状态为内部错误,若初始化成功则修改为成功
  *newcomm = comm; // 将通信对象赋值给传入的指针

  5、分配一个作业对象 job,并设置作业对象的各个成员变量
  NCCLCHECKGOTO(ncclCalloc(&job, 1), res, fail); // 为异步任务结构体分配内存
  job->comm = comm; // 设置异步任务的通信对象
  job->nranks = nranks; // 设置异步任务的节点数量
  job->commId = commId; // 设置异步任务的唯一ID
  job->myrank = myrank; // 设置异步任务的当前节点排名
  job->cudaDev = cudaDev; // 设置异步任务的CUDA设备号

  6、使用 ncclAsyncLaunch 异步启动 ncclCommInitRankFunc 函数来初始化通信。当这个函数完成时,它将自动调用 free 函数来释放 comm 对象     异步启动通信器初始化
  NCCLCHECKGOTO(ncclAsyncLaunch(&job->base, ncclCommInitRankFunc, NULL, free, comm), res, fail); // 异步启动初始化通信对象的任务

  7、退出和错误处理
exit:
  return ncclGroupErrCheck(res); // 返回最终的结果,检查是否有错误发生
fail:
  if (comm) { // 如果通信对象不为空
    if (comm->abortFlag) ncclCudaHostFree((void *)comm->abortFlag); // 如果存在中断标志,释放其内存
    if (comm->abortFlagRefCount) free(comm->abortFlagRefCount); // 释放中断标志引用计数的内存
    free(comm); // 释放通信对象的内存
  }
  if (newcomm) *newcomm = NULL; // 将通信对象指针设置为NULL
  goto exit; // 跳转到退出部分
}

主要步骤说明

前三步没啥好说的,主要就是检测状态,没有初始化的初始化,甚至不能算核心逻辑。

第4步说明:

第4步:4、配置NCCL通信器的一些属性,是否阻塞,通信通道数量等

直接看parseCommConfig()函数的源码会有点懵逼,那就直接看parseCommConfig()的最终结果,其实就是赋值了comm->config

源码速递

源码位置:nccl-master\src\init.cc

static ncclResult_t parseCommConfig(ncclComm_t comm, ncclConfig_t *config) {

  /*其它代码略*/


  //通信器配置赋值
  /* assign config to communicator */
  comm->config.blocking = internalConfigPtr->blocking;
  comm->config.cgaClusterSize = internalConfigPtr->cgaClusterSize;
  comm->config.minCTAs = internalConfigPtr->minCTAs;
  comm->config.maxCTAs = internalConfigPtr->maxCTAs;
  comm->config.netName = internalConfigPtr->netName;
  comm->config.splitShare = internalConfigPtr->splitShare;

  NCCLCHECKGOTO(envConfigOverride(comm), ret, fail);

exit:
  return ret;
fail:
  goto exit;
}
第6步说明:

异步启动通信器初始化

ncclCommInitRankFunc()核心逻辑:

1、获取 CUDA 设备和架构信息,初始化 CUDA 内核

2、是否有父通信器

        a、有,从父通信器分裂出来子通信器,并初始化

        b、无,直接为其分配内存,并初始化

3、设置通信器的CUDA架构版本和哈希值。

4、始化当前通信器的传输层。

5、加载调整器插件。调整器用于动态调整通信算法,以优化性能。

6、更新通信器状态为成功,表示通信器初始化成功

源码速递

源码位置:nccl-master\src\init.cc

// 定义了一个函数ncclCommInitRankFunc,用于初始化NCCL通信器的特定排名  
static ncclResult_t ncclCommInitRankFunc(struct ncclAsyncJob* job_) {  
    
  // 将传入的通用作业指针转换为特定于ncclCommInitRankAsyncJob的指针  
  struct ncclCommInitRankAsyncJob* job = (struct ncclCommInitRankAsyncJob*)job_;  
    
  // 获取作业中指定的通信器  
  ncclComm_t comm = job->comm;  
    
  // 初始化结果码为成功  
  ncclResult_t res = ncclSuccess;  
    
  // 初始化变量用于保存CUDA设备的计算能力的主版本和次版本  
  int archMajor, archMinor;  
    
  // 初始化变量用于保存内核栈的最大大小(字节为单位)  
  size_t maxLocalSizeBytes = 0;  
    
  // 获取作业中指定的CUDA设备ID  
  int cudaDev = job->cudaDev;  
    
  // 初始化一个指向父通信器排名的指针(初始为NULL)  
  int* parentRanks = NULL;  
  /  
  // 初始化一个变量用于保存CUDA设备的架构值(由主版本和次版本组成)  
  int cudaArch;  
  
  //
  1、获取 CUDA 设备和架构信息,初始化 CUDA 内核
  // 设置CUDA设备为指定的设备ID  
  CUDACHECKGOTO(cudaSetDevice(cudaDev), res, fail);  
    
  // 获取CUDA设备的主计算能力版本  
  CUDACHECKGOTO(cudaDeviceGetAttribute(&archMajor, cudaDevAttrComputeCapabilityMajor, cudaDev), res, fail);  
    
  // 获取CUDA设备的次计算能力版本  
  CUDACHECKGOTO(cudaDeviceGetAttribute(&archMinor, cudaDevAttrComputeCapabilityMinor, cudaDev), res, fail);  
    
  // 计算CUDA设备的架构值(例如,7.5对应750)  
  cudaArch = 100*archMajor + 10*archMinor;  
  
  // 为指定设备初始化NCCL内核  
  NCCLCHECK(ncclInitKernelsForDevice(cudaArch, &maxLocalSizeBytes));  
    
  // 如果内核栈的最大大小大于0且允许设置栈大小,则设置CUDA设备的栈大小限制  
  // 这可以避免在加载时重新配置CUDA内存(例如,NVSHMEM问题)  
  if (maxLocalSizeBytes > 0 && ncclParamSetStackSize() == 1) {  
    TRACE(NCCL_INIT, "Setting cudaLimitStackSize to %zi", maxLocalSizeBytes);  
    CUDACHECKIGNORE(cudaDeviceSetLimit(cudaLimitStackSize, maxLocalSizeBytes));  
  }  

  /
  /*2、是否有父通信器
        a、有,从父通信器分裂出来子通信器,并初始化
        b、无,直接为其分配内存,并初始化*/
  // 如果指定了父通信器  
  if (job->parent) {  
    // 为父通信器的排名分配内存  
    NCCLCHECKGOTO(ncclCalloc(&parentRanks, job->parent->nRanks), res, fail);  
      
    // 获取分裂信息,如新的排名数、当前排名、父通信器的排名等  
    NCCLCHECKGOTO(commGetSplitInfo(comm, job->parent, job->color, job->key, &job->nranks, &job->myrank, parentRanks), res, fail);  
      
    // 如果颜色为负,表示只是参与了一个allgather操作,但不需要创建新的通信器  
    // 我们已经完成了需要做的部分,因此跳转到exit标签  
    if (job->color == NCCL_SPLIT_NOCOLOR) goto exit;  
      
    // 生成一个唯一的通信器ID,该ID基于父通信器的哈希值  
    snprintf((char*)&job->commId, sizeof(job->commId), "%016lx-%d", job->parent->commHash, job->color);  
      
    // 分配通信器资源  
    NCCLCHECKGOTO(commAlloc(comm, job->parent, job->nranks, job->myrank), res, fail);  
      
    // 初始化分裂后的通信器  
    NCCLCHECKGOTO(bootstrapSplit((struct ncclBootstrapHandle*)&job->commId, comm, job->parent, job->color, job->key, parentRanks), res, fail);  
  } else {  
    // 如果没有指定父通信器,则为新的通信器分配资源  
    NCCLCHECKGOTO(commAlloc(comm, NULL, job->nranks, job->myrank), res, fail);  

    // 初始化新的通信器  

    NCCLCHECKGOTO(bootstrapInit((struct ncclBootstrapHandle*)&job->commId, comm), res, fail);  

}  
  
///
3、设置通信器的CUDA架构版本和哈希值。/

comm->cudaArch = cudaArch;  

// 根据通信器的唯一ID计算哈希值,并设置为通信器的哈希值  

comm->commHash = getHash(job->commId.internal, NCCL_UNIQUE_ID_BYTES);  

// 如果指定了父通信器,打印通信器的分裂信息  

if (job->parent) {  

  INFO(NCCL_INIT,"ncclCommSplit comm %p rank %d nranks %d cudaDev %d nvmlDev %d busId %lx parent %p color %d key %d commId 0x%llx - Init START",  

  comm, comm->rank, comm->nRanks, comm->cudaDev, comm->nvmlDev, comm->busId, job->parent, job->color, job->key, (unsigned long long)hashUniqueId(job->commId));  

} else {  

  // 如果没有指定父通信器,打印通信器的初始化信息  

  INFO(NCCL_INIT,"ncclCommInitRank comm %p rank %d nranks %d cudaDev %d nvmlDev %d busId %lx commId 0x%llx - Init START",  

  comm, comm->rank, comm->nRanks, comm->cudaDev, comm->nvmlDev, comm->busId, (unsigned long long)hashUniqueId(job->commId));  

}  
  
/
4、根据通信器的配置,初始化所有的传输层///  

NCCLCHECKGOTO(initTransportsRank(comm, job->parent), res, fail);  
  
/
5、加载NCCL的调优插件  //

NCCLCHECKGOTO(ncclTunerPluginLoad(&comm->tuner), res, fail);  

if (comm->tuner) {  

  // 如果成功加载了调优插件,调用其初始化函数  

  NCCLCHECK(comm->tuner->init(comm->nRanks, comm->nNodes, ncclDebugLog, &comm->tunerContext));  

}  
  
/
 6、更新通信器状态为成功,表示通信器初始化成功 /
// update communicator state  
comm->initState = ncclSuccess;

//
/到这里核心逻辑就结束了,已经完成通信器初始化,下面是日志跟踪等非核心逻辑///
  // Trace this call for replay tool
  if (job->parent) {
    /* unlink child abort flag. */
    __atomic_store_n(&job->parent->childAbortFlag, NULL, __ATOMIC_RELEASE);
    TRACE_CALL("ncclCommSplit(%p, %d, %d, %p, %d, %d)",
                job->parent, job->color, job->key, comm, comm->rank, comm->nRanks);
  } else {
    TRACE_CALL("ncclCommInitRank(%p, %d, 0x%llx, %d, %d)",
                comm, comm->nRanks, (unsigned long long)hashUniqueId(job->commId), comm->rank, comm->cudaDev);
  }

  if (job->parent) {
    INFO(NCCL_INIT,"ncclCommSplit comm %p rank %d nranks %d cudaDev %d nvmlDev %d busId %lx parent %p color %d key %d commId 0x%llx - Init COMPLETE",
    comm, comm->rank, comm->nRanks, comm->cudaDev, comm->nvmlDev, comm->busId, job->parent, job->color, job->key, (unsigned long long)hashUniqueId(job->commId));
  } else {
    INFO(NCCL_INIT,"ncclCommInitRank comm %p rank %d nranks %d cudaDev %d nvmlDev %d busId %lx commId 0x%llx - Init COMPLETE",
    comm, comm->rank, comm->nRanks, comm->cudaDev, comm->nvmlDev, comm->busId, (unsigned long long)hashUniqueId(job->commId));
  }
exit:
  if (job->newcomm) {
    /* assign it to user pointer. */
    __atomic_store_n(job->newcomm, comm, __ATOMIC_RELEASE);
  }
  free(parentRanks);
  return res;
fail:
  comm->initState = res;
  goto exit;
}

下期预告:

哈哈哈,到这我也是一脸懵逼,通信器在哪初始化的,没看到啊。请看下一次分享:

bootstrapInit()

NCCL源码详解4:bootstrapInit()引导网络bootstrap网络连接建立 视频教程_nccl bootstrap 过程-CSDN博客

### NCCL源码分析解释 NCCL (NVIDIA Collective Communications Library) 是专为高性能多GPU通信设计的库。该库提供了优化过的集体通信原语,如广播、规约、全规约等操作。 对于希望深入理解 NCCL 的开发者来说,可以从以下几个方面入手研究其源码: #### 1. 源码结构解析 NCCL 源码主要由几个核心模块组成: - `collective` 文件夹包了各种集合操作的具体实现[^1]。 - `transport` 负责处理不同设备间的数据传输逻辑。 - `topology` 提供了网络拓扑信息的支持功能。 这些部分共同协作来完成高效的跨 GPU 数据交换任务。 #### 2. 关键算法和技术要点 为了达到高效能的目标,NCCL 实现了一些关键技术特性: - **流水线机制**:通过将消息分割成多个片段并行发送接收,减少延迟影响。 - **分层架构**:支持多种底层硬件加速技术的选择和组合使用。 ```cpp // 示例代码展示如何初始化 NCCL 并执行一次 AllReduce 操作 #include <nccl.h> int main() { ncclComm_t comm; // 初始化 NCCL 环境... float *sendbuff, *recvbuff; size_t count=...; cudaMalloc(&sendbuff, ...); cudaMalloc(&recvbuff,...); ncclAllReduce(sendbuff, recvbuff, count, ncclFloat, ncclSum, comm, stream); // 清理资源... } ``` 上述代码展示了基本的 NCCL 使用方法,实际应用中还需要考虑更多细节配置选项以及错误处理等问题。 #### 3. 文档和支持材料 除了官方文档外,社区也贡献了许多有价值的参考资料可以帮助学习者更好地掌握 NCCL 工作原理及其内部运作方式: - NVIDIA 官方发布的白皮书和技术报告往往包最新的研究成果和发展方向介绍。 - GitHub 上有许多开源项目实现了基于 NCCL 构建的应用程序实例可供参考学习。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值