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;
}