NCCL源码详解6:通信拓扑识别感知构建 物理拓扑xml文件 ncclTopoGetSystem() 视频教程

Nvidia NCCL如何构建物理拓扑

视频教程在这:

2.2 NCCL源码分析:物理拓扑识别感知xml通信topo构建 ncclTopoGetSystem()_哔哩哔哩_bilibili

一、ncclTopoGetSystem()拓扑构建

1.1 ncclTopoGetSystem()拓扑构建核心逻辑:

1、 尝试从文件加载已有拓扑信息。

2、如果没有已有拓扑信息,创建一个名为"system"的根节点;

3、遍历本服务器所有GPU,拓扑树中添加GPU节点和NVlink;

4、遍历所有网络设备,拓扑树中添加网络拓扑节点;

5、移除不可用的节点;

6、Multi-Node NVLink (MNNVL) 跨服务器NVLink支持;

7、保存拓扑文件;

最最核心的就下面这三步

一、创建根节点

、遍历并插入GPU节点和NVlink

三、遍历并插入网卡节点

哈哈哈,原来如此简单!

ncclTopoGetSystem()中调用的最核心函数是啥,那必然是添加节点的函数,后面我们就来看看添加GPU节点ncclTopoFillGpu(),这里我们先过一下ncclTopoGetSystem()源码。

1.2 ncclTopoGetSystem()源码速递:

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

ncclResult_t ncclTopoGetSystem(struct ncclComm* comm, struct ncclTopoSystem** system) {  // 分配一个XML结构,用于存储拓扑信息  struct ncclXml* xml;  NCCLCHECK(xmlAlloc(&xml, NCCL_TOPO_XML_MAX_NODES));  ///1、 尝试从文件加载已有拓扑信息。// 尝试从环境变量中获取XML拓扑文件的路径  const char* xmlTopoFile = ncclGetEnv("NCCL_TOPO_FILE");  if (xmlTopoFile) {  // 如果环境变量设置了,则打印信息并加载该文件到xml结构中  INFO(NCCL_ENV, "NCCL_TOPO_FILE set by environment to %s", xmlTopoFile);  NCCLCHECK(ncclTopoGetXmlFromFile(xmlTopoFile, xml, 1));  } else {  // 如果没有设置环境变量,则尝试从默认位置加载XML拓扑文件  // Try default XML topology location  NCCLCHECK(ncclTopoGetXmlFromFile("/var/run/nvidia-topologyd/virtualTopology.xml", xml, 0));  }  /2、如果没有已有拓扑信息,创建一个名为"system"的根节点;//// 如果xml结构中没有任何节点(即没有加载到任何拓扑信息)  if (xml->maxIndex == 0) {  // 创建一个名为"system"的根节点,并设置其版本属性  // Create top tag  struct ncclXmlNode* top;  NCCLCHECK(xmlAddNode(xml, NULL, "system", &top));  NCCLCHECK(xmlSetAttrInt(top, "version", NCCL_TOPO_XML_VERSION));  }  /3、遍历本服务器所有GPU,拓扑树中添加GPU节点和NVlink;// 如果需要,自动检测GPU设备  // Auto-detect GPUs if needed  for (int r=0; r<comm->nRanks; r++) {  // 如果当前排名r对应的hostHash与当前rank的hostHash相同(可能是同一台机器上的不同GPU)  if (comm->peerInfo[r].hostHash == comm->peerInfo[comm->rank].hostHash) {  // 将busId转换为可读的PCI总线ID格式  char busId[NVML_DEVICE_PCI_BUS_ID_BUFFER_SIZE];  NCCLCHECK(int64ToBusId(comm->peerInfo[r].busId, busId));   // 填充一个表示GPU的XML节点  struct ncclXmlNode* node;  NCCLCHECK(ncclTopoFillGpu(xml, busId, &node));  // 如果没有成功创建节点,则继续下一次循环  if (node == NULL) continue;  // 设置该GPU节点的"keep"属性为1,表示需要保留这个节点  NCCLCHECK(xmlSetAttrInt(node, "keep", 1));  // 设置该GPU节点的"rank"属性为当前排名r  NCCLCHECK(xmlSetAttrInt(node, "rank", r));  // 设置该GPU节点的"gdr"属性,表示是否支持GPU Direct RDMA  NCCLCHECK(xmlInitAttrInt(node, "gdr", comm->peerInfo[r].gdrSupport));  }  }  /
4、遍历所有网络设备,拓扑树中添加网络拓扑节点;/
// 如果需要的话,自动检测NICs(网络接口卡)。net和collnet共享相同的xml/graph节点,  
// 所以我们先从collnet开始,以便它有更高的优先级。  
// Auto-detect NICs if needed. net/collnet share the same xml/graph nodes,  
// so we start with collnet so that it has precedence.  
int netDevCount = 0; // 初始化网络设备计数为0  
// 如果comm支持collNet  
if (collNetSupport(comm)) {  // 获取comm支持的网络设备数量  NCCLCHECK(collNetDevices(comm, &netDevCount));  // 遍历每个网络设备  for (int n=0; n<netDevCount; n++) {  ncclNetProperties_t props; // 定义一个ncclNetProperties_t类型的变量props,用于存储设备属性  // 获取第n个网络设备的属性  NCCLCHECK(collNetGetProperties(comm, n, &props));  // 创建一个XML节点来表示这个网络设备  struct ncclXmlNode* netNode;  // 使用设备的pci路径和名称来填充XML节点  NCCLCHECK(ncclTopoFillNet(xml, props.pciPath, props.name, &netNode));  // 将"keep"属性设置为1,可能表示这个节点需要被保留  NCCLCHECK(xmlSetAttrInt(netNode, "keep", 1));  // 将"dev"属性设置为n,表示这是第n个设备  NCCLCHECK(xmlSetAttrInt(netNode, "dev", n));  // 将速度、端口、GUID等属性添加到XML节点中  NCCLCHECK(xmlInitAttrInt(netNode, "speed", props.speed));  NCCLCHECK(xmlInitAttrInt(netNode, "port", props.port));  NCCLCHECK(xmlInitAttrUint64(netNode, "guid", props.guid));  NCCLCHECK(xmlInitAttrInt(netNode, "maxconn", props.maxComms));  // 检查是否支持GPU Direct RDMA(GDR)  bool gdrSupport = (props.ptrSupport & NCCL_PTR_CUDA) || (comm->dmaBufSupport && (props.ptrSupport & NCCL_PTR_DMABUF));  // 打印GDR支持状态和设备信息  INFO(NCCL_NET,"NET/%s : GPU Direct RDMA %s for HCA %d '%s'", comm->ncclNet->name, gdrSupport ? "Enabled" : "Disabled", n, props.name);  // 将GDR支持状态添加到XML节点中  NCCLCHECK(xmlInitAttrInt(netNode, "gdr", gdrSupport));  // 将"coll"属性设置为1,可能表示这是一个集合通信网络接口  NCCLCHECK(xmlInitAttrInt(netNode, "coll", 1));  }  
} // 循环遍历所有的网络设备,其中 netDevCount 是网络设备的总数  
for (int n=0; n<netDevCount; n++) {   // 定义一个 ncclNetProperties_t 类型的变量 props,用于存储网络设备的属性  ncclNetProperties_t props;  // 调用 getProperties 函数获取网络设备的属性,并检查调用是否成功  // 参数 n 是当前网络设备的索引,&props 是用于存储属性的指针  NCCLCHECK(comm->ncclNet->getProperties(n, &props));    // 定义一个指向 ncclXmlNode 结构的指针 netNode,该结构将用于表示 XML 中的节点  struct ncclXmlNode* netNode;   // 调用 ncclTopoFillNet 函数在 XML 结构中创建一个新的节点,并检查调用是否成功  // 参数 xml 是 XML 结构的指针,props.pciPath 和 props.name 是网络设备的 PCI 路径和名称  // &netNode 是用于存储新节点指针的指针  NCCLCHECK(ncclTopoFillNet(xml, props.pciPath, props.name, &netNode));  // 设置新节点的 keep 属性为 1,表示该节点应该被保留  NCCLCHECK(xmlSetAttrInt(netNode, "keep", 1));  // 设置新节点的 dev 属性为当前网络设备的索引 n  NCCLCHECK(xmlSetAttrInt(netNode, "dev", n));    // 设置新节点的 speed 属性为网络设备的速度  NCCLCHECK(xmlInitAttrInt(netNode, "speed", props.speed));  // 设置新节点的 port 属性为网络设备的端口号  // 并检查设置属性是否成功  NCCLCHECK(xmlInitAttrInt(netNode, "port", props.port));  // 设置新节点的 latency 属性为网络设备的延迟  NCCLCHECK(xmlInitAttrFloat(netNode, "latency", props.latency));    // 设置新节点的 guid 属性为网络设备的全局唯一标识符  NCCLCHECK(xmlInitAttrUint64(netNode, "guid", props.guid));   // 设置新节点的 maxconn 属性为网络设备支持的最大并发通信数  NCCLCHECK(xmlInitAttrInt(netNode, "maxconn", props.maxComms));  // 检查网络设备是否支持 GPU Direct RDMA  // 如果 props.ptrSupport 包含 NCCL_PTR_CUDA 或者如果 comm->dmaBufSupport 为真且 props.ptrSupport 包含 NCCL_PTR_DMABUF,则 gdrSupport 为真  bool gdrSupport = (props.ptrSupport & NCCL_PTR_CUDA) || (comm->dmaBufSupport && (props.ptrSupport & NCCL_PTR_DMABUF));   // 打印日志信息,显示网络设备是否支持 GPU Direct RDMA  // 其中 comm->ncclNet->name 是网络设备的名称,n 是设备的索引,props.name 是设备的名字  INFO(NCCL_NET,"NET/%s : GPU Direct RDMA %s for HCA %d '%s'", comm->ncclNet->name, gdrSupport ? "Enabled" : "Disabled", n, props.name);  // 设置新节点的 gdr 属性,表示是否支持 GPU Direct RDMA  NCCLCHECK(xmlInitAttrInt(netNode, "gdr", gdrSupport));  
}  5、移除不可用的节点;/
// 移除 XML 中不包含 keep="1" 节点的分支
NCCLCHECK(ncclTopoTrimXml(xml));  /
6、Multi-Node NVLink (MNNVL) 跨服务器NVLink支持;  
// 如果 MNNVL被启用  
if (comm->MNNVL) {  // MNNVL 集群支持  // 分配内存来存储所有集群成员的网络拓扑数据  char* mem;  // 为每个集群成员分配足够的内存空间来存储 XML 数据  // 假设每个成员的 XML 数据不超过 NCCL_TOPO_XML_MAX_NODES 大小  NCCLCHECK(ncclCalloc(&mem, comm->clique.size * xmlMemSize(NCCL_TOPO_XML_MAX_NODES)));   // 获取当前集群成员的 XML 数据区域  struct ncclXml* rankXml = (struct ncclXml*)(mem + xmlMemSize(NCCL_TOPO_XML_MAX_NODES) * comm->cliqueRank);  // 复制当前集群成员的 XML 数据  memcpy(rankXml, xml, xmlMemSize(NCCL_TOPO_XML_MAX_NODES));   // 将当前集群成员的 XML 数据转换为内部表示形式(可能是为了更高效的通信)  NCCLCHECK(ncclTopoConvertXml(rankXml, (uintptr_t)xml->nodes, 1));    // 在集群内所有成员间收集各自的 XML 数据  // bootstrapIntraNodeAllGather 可能是某种集群内收集数据的函数  NCCLCHECK(bootstrapIntraNodeAllGather(comm->bootstrap, comm->clique.ranks, comm->cliqueRank, comm->clique.size, mem, xmlMemSize(NCCL_TOPO_XML_MAX_NODES)));   // 分配一个新的 XML 结构来存储融合后的集群拓扑数据  struct ncclXml* cliqueXml;  NCCLCHECK(xmlAlloc(&cliqueXml, comm->clique.size * NCCL_TOPO_XML_MAX_NODES));   // 融合集群内所有成员的 XML 数据  for (int i = 0; i < comm->clique.size; i++) {  // 获取集群中每个成员的 XML 数据  struct ncclXml* peerXml = (struct ncclXml*)(mem + xmlMemSize(NCCL_TOPO_XML_MAX_NODES) * i);  // 将 XML 数据转换为内部表示形式(这次可能为了融合做准备)  NCCLCHECK(ncclTopoConvertXml(peerXml, (uintptr_t)peerXml->nodes, 0));  // 将当前成员的 XML 数据融合到 cliqueXml 中  NCCLCHECK(ncclTopoFuseXml(cliqueXml, peerXml));  }   // 释放原来的 XML 数据  free(xml);  // 更新 xml 指针以指向融合后的集群 XML 数据  xml = cliqueXml;  
}  //
7、保持拓扑文件;/  
// 获取环境变量 NCCL_TOPO_DUMP_FILE 的值,用于存储 XML 拓扑数据  
xmlTopoFile = ncclGetEnv("NCCL_TOPO_DUMP_FILE");  
// 如果环境变量被设置,并且当前进程是负责输出拓扑数据的进程(由 ncclParamTopoDumpFileRank() 确定)  
if (xmlTopoFile && comm->rank == ncclParamTopoDumpFileRank()) {  // 输出环境变量 NCCL_TOPO_DUMP_FILE 的值  INFO(NCCL_ENV, "NCCL_TOPO_DUMP_FILE set by environment to %s", xmlTopoFile);  // 将融合后的 XML 拓扑数据写入到指定的文件中  NCCLCHECK(ncclTopoDumpXmlToFile(xmlTopoFile, xml));  
}    
// 从 XML 数据中提取系统信息,并存储在 system 中  
// comm->peerInfo[comm->rank].hostHash 可能用于区分不同主机的哈希值  
NCCLCHECK(ncclTopoGetSystemFromXml(xml, system, comm->peerInfo[comm->rank].hostHash));  // 释放 XML 数据的内存  
free(xml);   
// 返回成功状态  
return ncclSuccess;}

ncclTopoGetSystem()中调用的最核心函数是添加节点的函数,下面我们就来看看添加GPU节点ncclTopoFillGpu()。

二、ncclTopoFillGpu核心逻辑:

2.1 ncclTopoFillGpu核心逻辑:

1、ncclTopoGetPciNode()确定当前GPU卡是否已创建xml node,没有就创建。

2、ncclTopoGetXmlFromSys()获取GPU到cpu的路径,路径信息获取,生成xml树。

3、GPU相关信息获取,设置NVlink信息。

2.2 源码速递:

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

// ncclTopoFillGpu,它接受一个ncclXml指针、一个char指针(busId)和一个ncclXmlNode指针的指针(gpuNode)作为参数  
ncclResult_t ncclTopoFillGpu(struct ncclXml* xml, const char* busId, struct ncclXmlNode** gpuNode) {  // 定义一个ncclXmlNode指针node,用于临时存储找到的PCI节点的指针  struct ncclXmlNode* node;  1、ncclTopoGetPciNode()确定当前GPU卡是否已创建xml node,没有就创建。// 调用ncclTopoGetPciNode函数,根据busId在xml结构中查找匹配的PCI节点,并将找到的节点指针存储在node中   NCCLCHECK(ncclTopoGetPciNode(xml, busId, &node));  // 调用xmlSetAttrIfUnset函数,确保node节点有"class"属性,并且其值为"0x03"  // 如果该属性已存在且值不是"0x03",则不会进行任何操作  NCCLCHECK(xmlSetAttrIfUnset(node, "class", "0x03"));  //2、ncclTopoGetXmlFromSys()获取GPU到cpu的路径,路径信息获取,生成xml树。// 调用ncclTopoGetXmlFromSys函数,从系统中获取与node节点相关的更多XML信息,并添加到xml结构中  NCCLCHECK(ncclTopoGetXmlFromSys(node, xml));  // 定义一个nvmlDevice_t类型的变量nvmlDev,用于存储从busId获取的NVML设备句柄  nvmlDevice_t nvmlDev;  // 调用ncclNvmlDeviceGetHandleByPciBusId函数,根据busId获取对应的NVML设备句柄,并存储在nvmlDev中  NCCLCHECK(ncclNvmlDeviceGetHandleByPciBusId(busId, &nvmlDev));  3、GPU相关信息获取,设置NVlink。// 调用ncclTopoGetXmlFromGpu函数,从nvmlDev设备句柄中获取与GPU相关的XML信息,并添加到xml结构中  // 同时,将找到的GPU节点的指针存储在gpuNode中  NCCLCHECK(ncclTopoGetXmlFromGpu(node, nvmlDev, xml, gpuNode));  // 如果以上所有操作都成功完成,则返回ncclSuccess表示成功  return ncclSuccess;  
}
2.3 ncclTopoGetPciNode()

我们过一下第一步中的函数ncclTopoGetPciNode()

1、ncclTopoGetPciNode()确定当前GPU卡是否已创建xml node,没有就创建。

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

// 定义一个函数ncclTopoGetPciNode,它接受一个ncclXml结构体指针xml,一个字符串指针busId用于指定PCI节点的busid,  
// 以及一个指向ncclXmlNode指针的指针pciNode,用于返回找到的或新创建的PCI节点的地址。  
ncclResult_t ncclTopoGetPciNode(struct ncclXml* xml, const char* busId, struct ncclXmlNode** pciNode) {  // 调用xmlFindTagKv函数在xml中查找标签为"pci"且属性"busid"等于busId的节点。  // 如果找到,将找到的节点的地址存储在*pciNode中。   NCCLCHECK(xmlFindTagKv(xml, "pci", pciNode, "busid", busId));  // 如果*pciNode是NULL,表示没有找到与busId相对应的PCI节点。  if (*pciNode == NULL) {  // 调用xmlAddNode函数在xml中添加一个新的"pci"节点,并将其地址存储在*pciNode中。  // 这里的NULL作为父节点参数,意味着新节点将被添加到XML树的根目录下。  NCCLCHECK(xmlAddNode(xml, NULL, "pci", pciNode));  // 调用xmlSetAttr函数设置新创建的PCI节点的"busid"属性为busId。  NCCLCHECK(xmlSetAttr(*pciNode, "busid", busId));  }  // 函数成功完成,返回ncclSuccess表示操作成功。  return ncclSuccess;  
}
2.4 ncclTopoGetXmlFromSys()

当然ncclTopoFillGpu中调用的最最核心的,还是这个函数ncclTopoGetXmlFromSys()。

2.4.1 ncclTopoGetXmlFromSys()核心逻辑

1、getPciPath()获取GPU到cpu的路径;

2、获取link_width,link_speed等属性;

3、根据路径查找父节点,查找不到就创建父节点,继续查找父节点的父节点(爷爷节点),就这样循环查找和创建,构建xml树,直到找到父节点;

4、插入节点GPU节点。

2.4.2 ncclTopoGetXmlFromSys()源码速递
ncclResult_t ncclTopoGetXmlFromSys(struct ncclXmlNode* pciNode, struct ncclXml* xml) {  // 从pciNode节点中获取busid属性  const char* busId;  NCCLCHECK(xmlGetAttr(pciNode, "busid", &busId)); // 分配一个字符指针path,用于存储PCI设备的路径(初始化为NULL)  char* path = NULL;  // 临时设置ncclDebugNoWarn为NCCL_GRAPH,可能是为了关闭某些与图相关的警告  ncclDebugNoWarn = NCCL_GRAPH;  ///1、getPciPath()获取GPU到cpu的路径;// 调用getPciPath函数,根据busId获取PCI设备的路径,并存储在path中  getPciPath(busId, &path);  // 恢复ncclDebugNoWarn为0,关闭之前的特殊调试设置  ncclDebugNoWarn = 0;   // 如果path不为空(即成功获取了PCI设备的路径)  if (path) {  // 调用ncclTopoSetAttrFromSys函数,从系统中获取与path对应的"class"属性,并设置到pciNode节点中  NCCLCHECK(ncclTopoSetAttrFromSys(pciNode, path, "class", "class"));  }  /2、获取link_width,link_speed等属性;/// 以下代码块类似地处理其他PCI设备属性:vendor, device, subsystem_vendor, subsystem_device  int index;  ncclDebugNoWarn = NCCL_GRAPH;  // 尝试从pciNode节点中获取vendor属性的索引  NCCLCHECK(xmlGetAttrIndex(pciNode, "vendor", &index));  // 如果索引为-1(即属性不存在)  if (index == -1) {  // 如果path不为空,则从系统中获取与path对应的"vendor"属性,并设置到pciNode节点中  if (path) ncclTopoSetAttrFromSys(pciNode, path, "vendor", "vendor");  }    // ...(类似地处理device, subsystem_vendor, subsystem_device属性)  // 尝试从pciNode节点中获取link_speed属性的索引  NCCLCHECK(xmlGetAttrIndex(pciNode, "link_speed", &index));  // 如果索引为-1(即属性不存在)  if (index == -1) {  // 如果path不为空  if (path) {  // 定义两个字符串数组用于存储从系统中读取的最大链路速度  char deviceSpeedStr[MAX_STR_LEN];  char portSpeedStr[MAX_STR_LEN];  // 初始化设备速度和端口速度为FLT_MAX(一个表示无穷大的浮点数)  float deviceSpeed = FLT_MAX;  float portSpeed = FLT_MAX;  // 从系统中获取与path对应的"max_link_speed"属性,并存储在deviceSpeedStr中  NCCLCHECK(ncclTopoGetStrFromSys(path, "max_link_speed", deviceSpeedStr));  // 将字符串转换为浮点数  sscanf(deviceSpeedStr, "%f GT/s", &deviceSpeed);  // 类似地,从系统的父路径(可能是端口)中获取"max_link_speed"属性  NCCLCHECK(ncclTopoGetStrFromSys(path, "../max_link_speed", portSpeedStr));  sscanf(portSpeedStr, "%f GT/s", &portSpeed);  // 设置link_speed属性为设备速度和端口速度中较小的一个  NCCLCHECK(xmlSetAttr(pciNode, "link_speed", portSpeed < deviceSpeed ? portSpeedStr : deviceSpeedStr));  } else {  // 如果path为空,则设置link_speed属性为空字符串  NCCLCHECK(xmlSetAttr(pciNode, "link_speed", ""));  }  } NCCLCHECK(xmlGetAttrIndex(pciNode, "link_width", &index));if (index == -1) {if (path) {char strValue[MAX_STR_LEN];NCCLCHECK(ncclTopoGetStrFromSys(path, "max_link_width", strValue));int deviceWidth = strtol(strValue, NULL, 0);NCCLCHECK(ncclTopoGetStrFromSys(path, "../max_link_width", strValue));int portWidth = strtol(strValue, NULL, 0);NCCLCHECK(xmlSetAttrInt(pciNode, "link_width", std::min(deviceWidth,portWidth)));} else {NCCLCHECK(xmlSetAttr(pciNode, "link_width", ""));}}/
3、根据路径查找父节点,查找不到就创建父节点,继续查找父节点的父节点(爷爷节点),就这样循环查找和创建,构建xml树,直到找到父节点;//
struct ncclXmlNode* parent = pciNode->parent; // 获取当前pciNode的父节点  
if (parent == NULL) { // 如果当前pciNode没有父节点  if (path) { // 如果path(PCI设备的路径)不为空  // Save that for later in case next step is a CPU  char numaIdStr[MAX_STR_LEN]; // 用于存储NUMA节点ID的字符串// 从系统中根据PCI路径获取NUMA节点ID    NCCLCHECK(ncclTopoGetStrFromSys(path, "numa_node", numaIdStr)); // Go up one level in the PCI tree. Rewind two "/" and follow the upper PCI  // switch, or stop if we reach a CPU root complex.  int slashCount = 0; // 用于计数'/'的数量  int parentOffset; // 用于遍历path的偏移量  for (parentOffset = strlen(path)-1; parentOffset>0; parentOffset--) { // 从path的末尾开始遍历  if (path[parentOffset] == '/') { // 如果遇到'/'  slashCount++; // '/'计数加1  path[parentOffset] = '\0'; // 临时将'/'替换为'\0',以便获取上级路径  int start = parentOffset - 1; // 从'/'前一个字符开始寻找下一个'/'  while (start>0 && path[start] != '/') start--; // 找到上一个'/'  // 检查上级路径是否符合PCI设备的BDF格式(总线:设备:功能)// Check whether the parent path looks like "BBBB:BB:DD.F" or not.  if (checkBDFFormat(path+start+1) == 0) {   // This a CPU root complex. Create a CPU tag and stop there.  struct ncclXmlNode* topNode; // 定义一个指向系统顶层的指针  NCCLCHECK(xmlFindTag(xml, "system", &topNode)); // 查找系统标签  NCCLCHECK(xmlGetSubKv(topNode, "cpu", &parent, "numaid", numaIdStr)); // 在系统标签下查找numaid为numaIdStr的cpu标签  if (parent == NULL) { // 如果没有找到  NCCLCHECK(xmlAddNode(xml, topNode, "cpu", &parent)); // 在系统标签下添加一个新的cpu标签  NCCLCHECK(xmlSetAttrLong(parent, "host_hash", getHostHash())); // 设置host_hash属性  NCCLCHECK(xmlSetAttr(parent, "numaid", numaIdStr)); // 设置numaid属性  }  } else if (slashCount == 2) { //   // Continue on the upper PCI switch  for (int i = strlen(path)-1; i>0; i--) { // 从path的末尾开始遍历  if (path[i] == '/') { // 如果遇到'/'  NCCLCHECK(xmlFindTagKv(xml, "pci", &parent, "busid", path+i+1)); // 查找busid为path+i+1的pci标签  if (parent == NULL) { // 如果没有找到  NCCLCHECK(xmlAddNode(xml, NULL, "pci", &parent)); // 在XML树的根下添加一个新的pci标签  NCCLCHECK(xmlSetAttr(parent, "busid", path+i+1)); // 设置busid属性  }  break; // 找到后跳出循环  }  }  }  }  if (parent) break; // 如果找到了父节点,则跳出循环  }  } else {  // 如果没有在 /sys 目录下找到相关信息,那么将 GPU 附加到一个未知的 CPU 上  // No information on /sys, attach GPU to unknown CPU    // 尝试在 XML 中查找名为 "cpu" 的标签,并查找具有 "numaid" 属性的标签,如果找不到,则 "numaid" 设置为 "-1"  NCCLCHECK(xmlFindTagKv(xml, "cpu", &parent, "numaid", "-1"));  // 如果 parent 指针为空,说明没有找到符合条件的 "cpu" 标签  if (parent == NULL) {  // 定义一个指向 XML 节点的指针 topNode  struct ncclXmlNode* topNode;   // 在 XML 中查找名为 "system" 的标签,并将找到的节点赋值给 topNode  NCCLCHECK(xmlFindTag(xml, "system", &topNode));   // 在 topNode 下添加一个名为 "cpu" 的子节点,并将新节点的指针赋值给 parent  NCCLCHECK(xmlAddNode(xml, topNode, "cpu", &parent));  // 设置新节点的 "host_hash" 属性为当前主机的哈希值  NCCLCHECK(xmlSetAttrLong(parent, "host_hash", getHostHash()));  // 设置新节点的 "numaid" 属性为 "-1",表示这是一个未知的 CPU  NCCLCHECK(xmlSetAttr(parent, "numaid", "-1"));  // 调用函数从 CPU 获取 XML 数据并填充到 parent 节点中  NCCLCHECK(ncclTopoGetXmlFromCpu(parent, xml));  }  ///4、插入节点GPU节点。/// 将 pciNode 的 parent 指针设置为前面找到的或创建的 parent 节点  pciNode->parent = parent;  // 为了保持 PCI 子设备按 PCI 总线 ID 排序(解决 Issue #820)  // 首先获取新 PCI 设备的 busid  int subIndex = parent->nSubs; // 假设新设备被添加到子节点的末尾  const char* newBusId;  NCCLCHECK(xmlGetAttrStr(pciNode, "busid", &newBusId)); // 获取新 PCI 设备的 busid  // 遍历 parent 的所有子节点  for (int s=0; s<parent->nSubs; s++) {  // 获取当前子节点的 busid  const char* busId;  NCCLCHECK(xmlGetAttr(parent->subs[s], "busid", &busId));  // 如果当前子节点的 busid 不为空,并且新设备的 busid 比当前子节点的 busid 小  // 则将 subIndex 设置为当前索引 s,并跳出循环  if (busId != NULL && strcmp(newBusId, busId) < 0) { subIndex = s; break; }  }  // 如果 parent 的子节点数量已经达到了最大值 MAX_SUBS  if (parent->nSubs == MAX_SUBS) {  // 发出警告,并返回 ncclInternalError 错误  WARN("Error : XML parser is limited to %d subnodes", MAX_SUBS);  return ncclInternalError;  }  // 将 parent 的子节点从 subIndex 开始向后移动一个位置,为新节点腾出空间  for (int s = parent->nSubs; s > subIndex; s--) parent->subs[s] = parent->subs[s-1];  // 将 pciNode 插入到正确的位置 subIndex  parent->subs[subIndex] = pciNode;  // 增加 parent 的子节点数量  parent->nSubs++;  }  // 如果 parent 节点的名称是 "pci",则从系统获取 XML 数据并填充到 parent 节点中  if (strcmp(parent->name, "pci") == 0) {  NCCLCHECK(ncclTopoGetXmlFromSys(parent, xml));  }   // 如果 parent 节点的名称是 "cpu",则从 CPU 获取 XML 数据并填充到 parent 节点中  else if (strcmp(parent->name, "cpu") == 0) {  NCCLCHECK(ncclTopoGetXmlFromCpu(parent, xml));  }  // 释放之前分配给 path 的内存  free(path);  // 返回 ncclSuccess 表示操作成功  return ncclSuccess;  
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/43210.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【解决Windows11系统Windows Hello不能使用的问题】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、Windows Hello是什么&#xff1f;二、使用步骤1.购买一个摄像头2.开始配置 三、异常解决1.内置管理员不能使用2.没找到合适的摄像头3.摄像头需要专用驱动4.…

原创作品——教育课程界面设计

教育行业UI界面设计需直观易懂&#xff0c;确保学习者能迅速上手&#xff0c;减少认知负担。布局清晰&#xff0c;导航便捷&#xff0c;功能按钮和图标设计应符合教育场景&#xff0c;直接支持学习目标的达成&#xff0c;促进高效学习体验。 通过丰富的互动元素&#xff08;如拖…

博途通讯笔记1:1200与1200之间S7通讯

目录 一、添加子网连接二、创建PUT GET三、各个参数的意义 一、添加子网连接 二、创建PUT GET 三、各个参数的意义

代码随想录(day1)二分法

if语句的基本语法 if 要判断的条件: 条件成立的时候&#xff0c;要做的事举例&#xff1a; if nums[middle]<target:leftmiddle1 while语句的基本语法&#xff1a; while 判断条件(condition)&#xff1a;执行语句(statements)举例&#xff1a; while left<right:midd…

docker安装mysql8.0.23

拉取镜像 docker pull mysql:8.0.23 创建挂载文件 mkdir -p /home/docker/mysql/conf mkdir -p /home/docker/mysql/data mkdir -p /home/docker/mysql/logcd /home/docker/mysql/conf touch my.cnf#授权 chmod 777 -R /home/docker/mysql/conf chmod 777 -R /home/docker/m…

C++类的成员:静态成员变量、静态成员函数、非静态成员变量、非静态成员函数

(1) 类的成员 A.What&#xff08;什么是类的成员&#xff09; 是组成类的基本构建&#xff0c;包含数据成员、静态成员和 成员函数 B.Which&#xff08;类的成员有哪些&#xff09; 数据成员&#xff1a; 用于存储与类的对象相关的数据&#xff0c;例如整数、浮点数、字符串、…

2通道音频ADC解码芯片ES7243L、ES7243E、ES7243,用于低成本实现模拟麦克风转换为IIS数字话筒

前言&#xff1a; 音频解码芯片某创参考价格&#xff1a; ES7243L 500&#xff1a;&#xffe5;1.36 / 个 ES7243E 500&#xff1a;&#xffe5;1.66 / 个 ES7243 500&#xff1a; &#xffe5;1.91 / 个 其中ES7243L工作电压为1.8V&#xff0c;与其他两款的3.3V工作电压不同&…

ESP32-C3模组上跑通AES-GCM(2)

接前一篇文章:ESP32-C3模组上跑通AES-GCM(1) 本文内容参考: mbedtls学习笔记 AES GCM_aes128 gcm的aad是什么-CSDN博客 https://www.cnblogs.com/testlearn/p/16547583.html 对称加密和非对称加密,一文讲解明白!-CSDN博客 深入理解高级加密标准(Advanced Encryption…

日本IT-SIER/SES的区别详情、契约形态等

一、SLER 主要的服务内容就是“帮客人开发系统或是各种APP&#xff0c;并在指定期间内交货&#xff0c;交货后也会持续进行运维等售后服务”。 客人很广泛&#xff0c;小到普通的服务业商家&#xff08;餐饮店/服饰店/美容业/电商&#xff09;大到各种公共/政府机关&#xff…

【面试题】串联探针和旁挂探针有什么区别?

在网络安全领域中&#xff0c;串联探针和旁挂探针&#xff08;通常也被称为旁路探针&#xff09;是两种不同部署方式的监控设备&#xff0c;它们各自具有独特的特性和应用场景。以下是它们之间的主要区别&#xff1a; 部署方式 串联探针&#xff1a;串联探针一般通过网关或者…

`padding`、`border`、`width`、`height` 和 `display` 这些 CSS 属性的作用

盒模型中的属性 padding&#xff08;内边距&#xff09; padding 用于控制元素内容与边框之间的空间&#xff0c;可以为元素的每个边&#xff08;上、右、下、左&#xff09;分别设置内边距。内边距的单位可以是像素&#xff08;px&#xff09;、百分比&#xff08;%&#xf…

Lambda架构与Kappa架构的特性对比

一个大数据系统架构的设计思想很大程度上受到当时技术条件和思维模式的限制。Lambda架构将批处理层和速度层分为两层&#xff0c;分别进行离线数据处理和实时数据处理&#xff0c;这样设计的根本原因在于&#xff0c;Lambda提出的初期是在公司中进行小范围的业务运用&#xff0…

从Java开发者到.NET Core初级工程师学习路线:C#语言基础

1. C#语言基础 1.1 C#语法概览 欢迎来到C#的世界&#xff01;对于刚从Java转过来的开发者来说&#xff0c;你会发现C#和Java有很多相似之处&#xff0c;但C#也有其独特的魅力和强大之处。让我们一起来探索C#的基本语法&#xff0c;并比较一下与Java的异同。 程序结构 C#程序…

MySQL-数据库管理:优化、安全、合规与迁移的全面解析

1. 数据库设计 1.1 需求分析 数据库设计的第一步是深入理解应用的需求。这通常涉及到与项目团队&#xff08;包括产品经理、开发者、业务分析师等&#xff09;的紧密合作&#xff0c;以确保数据库设计能够准确地反映业务需求。需求分析阶段的关键活动包括&#xff1a; 收集信…

第一百四十九节 Java数据类型教程 - Java子字符串、字符串转换

Java数据类型教程 - Java子字符串 获取子字符串 我们可以使用substring()方法来获取字符串的子部分。 我们可以将开始索引作为参数&#xff0c;并返回一个从开始索引开始到字符串结尾的子串。 我们还可以将开始索引和结束索引作为参数。 它返回从开始索引开始的子字符串和小…

linux高级编程(文件I/O)

标准I/O与文件I/O的区别&#xff1a; 文件操作: 缓存 操作对象 具体操作 标准IO 全缓存/行缓存 文件指针(流指针)FILE * 1.打开 --fopen 2.读写 …

Windows右键没有新建Word、PPT与Excel的解决方法

本文介绍在Windows电脑中&#xff0c;右键与资源管理器的“新建”选项中&#xff0c;都没有新建Word、PPT或Excel文件的解决方法。 最近&#xff0c;发现一台重装了系统与Office的电脑中&#xff0c;无论是桌面上与资源管理器中的右键&#xff0c;还是资源管理器左侧顶部的“新…

力扣1546.和为目标值且不重叠的非空子数组的最大数目

力扣1546.和为目标值且不重叠的非空子数组的最大数目 从头开始找 找到满足条件的就清空哈希表 class Solution {public:int maxNonOverlapping(vector<int>& nums, int target) {int n nums.size();vector<int> s(n1);for(int i0;i<n;i) s[i1] s[i] n…

手写简单模拟mvc

目录结构&#xff1a; 两个注解类&#xff1a; Controller&#xff1a; package com.heaboy.annotation;import java.lang.annotation.*;/*** 注解没有功能只是简单标记* .RUNTIME 运行时还能看到* .CLASS 类里面还有&#xff0c;构建对象久没来了&#xff0c;这个说明…

掌握Vue 3生命周期:从组合式API到高效代码实践

引言 在 Vue 3 中&#xff0c;生命周期的概念得到了进一步的优化和简化。Vue 3 引入了组合式 API&#xff08;Composition API&#xff09;&#xff0c;这为开发者提供了更灵活的方式来组织和重用代码逻辑。与传统的选项式 API&#xff08;Options API&#xff09;相比&#x…