NCCL-AllReduce-任务分类、聚合与调优 (四)

1. 概述

NCCL 根据 Group 边界、集合通信操作参数和单次调用配置,对用户登记的集合通信任务进行分类、聚合与调优。同一聚合组内的任务共享调优结果;后续编排为各任务分配 Channel,并将对应的工作项分组,供设备执行。下图采用第 2.1 节的 A/B/C/D 示例,展示同一 communicator 内 AllReduce 传统内核路径从任务登记到设备执行的过程,其中 Plan 组合与 Channel 分段仅作结构示意。

  1. 接口输入。 调用方在 ncclGroupStart 与 ncclGroupEnd 之间发起一批集合通信调用。每次 ncclAllReduce 调用指定归约操作、数据类型和元素数量,ncclAllReduceConfig 还可设置本次调用的资源约束。NCCL 根据这些输入确定任务类别和聚合范围;图中 D 设置 maxCTAs=4,因此被标记为需要独立调优的任务。
  2. 任务分类。 每次调用对应一个 ncclTaskColl。collSorter 根据 trafficBytes 将任务放入不同的流量区间,再按区间从大到小取出任务;同一区间内采用头插法,因此得到的是近似排序。随后,NCCL 将选用对称内核的任务移入独立队列,再将其余任务按 (func, opDev.op, datatype) 归类。图中 A、B、D 属于同一类别,C 因数据类型不同进入另一类别;两条执行路径的分流依据见第 4.3 节。
  3. 聚合调优。 NCCL 在同一类别的链表中划分连续的聚合区间。候选任务的流量严格小于区间首任务流量的 4 倍,且两者均未设置隔离标记时,候选任务可以加入当前区间。对于图中的 A、B,NCCL 将首任务复制为 ncclTaskColl 类型的临时聚合任务 agg,再累加两者的 count 和 trafficBytes,共享一次 ncclGetAlgoInfo 调用;C、D 则分别调用。三次调优的输入 nBytes 分别为 18、12、18 KiB,均按聚合后的元素数量与类型大小计算。在多 rank 场景下,ncclGetAlgoInfo 根据配置、拓扑和算法能力筛选可用的算法与协议组合,估算各组合的通信耗时,并在存在 tuner 时由插件调整估算耗时;随后选择估算耗时最短的组合,计算 Channel 上限和 warp 数,将结果写入 agg。随后,ncclPrepareTasks 补齐设备函数标识和调度标志,写回各原任务,再按调度约束组织 collTaskQueue。各任务仍保留自己的元素数量和缓冲区引用。
  4. 工作编排。 主机为各任务构造或复用 Work,保存缓冲区地址和归约参数。计划编排按容量和资源约束将 Work 组织到 Plan 中,并为任务分配 Channel 和数据分段。主机为每个 Channel 组织待执行的工作项,并用 Batch 记录这些工作项的位置及其共用的设备函数。图中 D 独占一个 Plan,其 maxCTAs=4 在这一阶段限制 Channel 数量;Plan P 的两个 Batch 都引用 Work A 和 Work B,每份 Work 在 Plan 中只保存一次。
  5. 主机传递。 主机通过 kernelArgs 传入 Channel 掩码和 Batch 描述,并根据参数区容量与图捕获状态,将 Work 存放在参数区、FIFO 或持久设备缓冲区中。Work 存放在参数区之外时,kernelArgs 中的 workBuf 提供其访问地址。设备通过这些描述取得执行参数,再通过 Work 中的地址访问各任务的用户缓冲区。
  6. 设备执行。 设备内核将每个 Block 映射到一个 Channel,按 Batch 加载 Work,再依据 Batch 中的 funcId 选择相应的设备函数。设备函数逐项执行 Work,使用各任务自己的输入输出缓冲区,处理当前 Channel 负责的数据分段。

2. 关键接口与对象

用户通过 Group 边界、集合通信调用参数和单次调用配置影响任务分类和聚合。本文将任务整理、分类、聚合调优和连接需求记录统称为任务预处理。

2.1 用户配置接口

应用通过集合通信操作参数、Group 边界和单次调用配置影响任务分类与聚合。任务类别和聚合区间由 NCCL 根据这些输入确定,公开接口不提供直接指定分类 ID 或聚合组编号的字段。

接口定义 功能
ncclResult_t ncclGroupStart() 开始一层 Group,收集后续 NCCL 调用;支持嵌套。
ncclResult_t ncclGroupEnd() 结束一层 Group;最外层结束时触发已登记调用的后续处理,GPU 操作的完成时机仍由 CUDA stream 语义确定。
ncclResult_t ncclAllReduce(const void* sendbuff, void* recvbuff, size_t count, ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm, cudaStream_t stream) 登记一次 AllReduce 请求,使用默认的单次调用配置。
ncclResult_t ncclAllReduceConfig(const void* sendbuff, void* recvbuff, size_t count, ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm, cudaStream_t stream, const ncclCollConfig_t* config) 登记一次 AllReduce 请求,并传入本次调用的配置;config 为空时等价于普通接口。

ncclCollConfig_t 描述单次集合通信调用的资源约束、算法选择和附加设置,由调用方创建,并通过配置接口的 config 参数传入。配置对象及其扩展链表在使用它们的 API 调用期间必须保持有效,各 rank 应使用一致的配置。创建对象时必须使用 NCCL_COLLCONFIG_INITIALIZER 初始化,再修改需要设置的字段。

cpp 复制代码
typedef struct ncclCollConfig_v23200 {
  // 结构大小,由初始化宏填写,用户不应修改。
  size_t size;

  // 配置校验标识,由初始化宏填写,用户不应修改。
  unsigned int magic;

  // 配置版本,由初始化宏填写,用户不应修改。
  unsigned int version;

  // 厂商扩展配置链表,链表不能包含环。
  ncclConfigExt_t* ext;

  // Channel/CTA 数量下限。
  // NCCL_CONFIG_UNDEF_INT 表示未显式设置。
  int minCTAs;

  // Channel/CTA 数量上限,受 communicator 上限约束。
  // NCCL_CONFIG_UNDEF_INT 表示未显式设置。
  int maxCTAs;

  // NVLS 专用 Channel 资源上限。
  // NCCL_CONFIG_UNDEF_INT 表示未显式设置。
  int nvlsCTAs;

  // CUDA 线程块集群大小,适用于 Hopper 及更新架构。
  // NCCL_CONFIG_UNDEF_INT 表示未显式设置。
  // 同一 Group 中各集合调用的设置应保持一致。
  int cgaClusterSize;

  // 本次调用允许使用的算法或内核选择字符串。
  // NULL、NCCL_CONFIG_UNDEF_PTR 或空字符串表示自动选择。
  const char* algSelection;

  // 是否要求满足 algSelection。
  // 默认值为 1:无法满足时返回错误。
  // 设为 0:允许回退到自动选择。
  int forceAlgSelection;

  // NCCL_CTA_POLICY_* 策略位掩码。
  // NCCL_CONFIG_UNDEF_INT 表示继承 communicator 的策略。
  int CTAPolicy;

  // 传递给 profiler 插件的用户标签,默认值为 0。
  // 最高位由 NCCL 保留,应用设置的标签应保持该位为 0。
  uint64_t userProfilerTag;

  // 调用方持有的、rank 本地的 CUDA event。
  // 在集合内核启动完成时记录;CUDA < 12.3 时在启动前记录。
  // 创建时使用 cudaEventDisableTiming,不支持进程间和互操作事件。
  // 各 rank 必须同时提供事件或同时不提供事件。
  // 同一 Group 中每个 communicator 最多支持一个非空事件。
  // 事件须保持有效,直到所有相关等待及 Graph 执行结束。
  cudaEvent_t launchCompletionEvent;
} ncclCollConfig_t;

这些输入分别决定候选任务范围、任务类别和共享调优范围。

  1. Group 与 communicator 确定候选范围。 同一 Group 可以收集多个 communicator 的请求,各 communicator 使用自己的 planner,分类和聚合在各自的任务集合内完成。嵌套 Group 在最外层 ncclGroupEnd 才触发处理。
  2. 操作参数确定类别和规模。 调用的集合类型、归约操作和数据类型形成内部分类键 (func, opDev.op, datatype),其中 opDev.op 是转换后的设备归约操作。AllReduce 的流量估计为 trafficBytes = count × ncclTypeSize(datatype) × 2;count 影响规模判断,不属于分类键。
  3. 单次调用配置约束共享范围。 显式设置 minCTAs、maxCTAs、nvlsCTAs、非空 algSelection 或 launchCompletionEvent,以及解析后的 CTAPolicy 不同于 communicator 默认值,都会使任务设置 aggIsolate。这类任务独立调优,使本次调用的配置只作用于自己的调优描述。仅设置 userProfilerTag 或 cgaClusterSize 本身不触发隔离;cgaClusterSize 是整个 launch 的属性,同一 Group 应保持一致。

本节示例假设在同一 Group 内,使用同一 communicator 和同一 CUDA stream 依次登记 A、B、C、D 四个请求,且均使用 ncclSum 并进入传统任务路径。各任务使用各自的设备缓冲区,各 rank 使用一致的调用顺序、操作参数和配置;communicator 允许至少 4 个 CTA,且没有环境变量覆盖单次调用配置。D 的配置先按公开接口要求初始化,再设置上限:

cpp 复制代码
ncclCollConfig_t config = NCCL_COLLCONFIG_INITIALIZER;
config.maxCTAs = 4;

调用方在 ncclGroupStart 与 ncclGroupEnd 之间发出下表请求,D 向配置接口传入 &config。

任务 调用接口 datatype count 单次调用配置 trafficBytes
A ncclAllReduce ncclFloat32 1536 默认 12 KiB
B ncclAllReduce ncclFloat32 3072 默认 24 KiB
C ncclAllReduce ncclFloat64 1536 默认 24 KiB
D ncclAllReduceConfig ncclFloat32 4608 maxCTAs=4 36 KiB

A、B、D 进入同一操作类别,C 因数据类型不同进入另一类别。A 与 B 满足严格的 4 倍规模条件,使用临时 agg 共享一次调优;D 虽然规模接近,也因配置隔离而独立调优。各原任务继续保留自己的元素数量和缓冲区引用。

2.2 任务预处理接口

接口定义 功能
static ncclResult_t ncclPrepareTasksAndCollPreconnect(struct ncclComm* comm, ncclSimInfo_t* simInfo, struct ncclIntruQueue<struct ncclAsyncJob, &ncclAsyncJob::next>* asyncCollJobs) 根据配置,直接执行任务预处理,或将相应的 job 加入队列。直接执行时,根据预处理结果按需追加连接 job。
ncclResult_t ncclPrepareTasksAndCollPreconnectFunc(struct ncclAsyncJob* job_) 作为预处理 job 的回调,设置设备上下文并执行任务预处理,再按条件调用 Collective 连接入口。
ncclResult_t ncclPrepareTasks(struct ncclComm* comm, bool* algoNeedConnect, bool* needConnect, ncclSimInfo_t* simInfo) 整理并调优任务,更新 planner 和任务字段,按需置位连接输出;返回主机预处理阶段的状态码。
static ncclResult_t asyncJobLaunch(struct ncclIntruQueue<struct ncclAsyncJob, &ncclAsyncJob::next>* asyncJobsMain, volatile bool* groupAbortFlag) 根据 job 数量选择当前线程或多个线程执行,等待本批工作完成并汇总状态。
ncclResult_t ncclCollPreconnect(struct ncclComm* comm, bool* algoNeedConnect) 遍历已置位的算法需求,调用相应连接或缓冲区准备入口。
static ncclResult_t collTaskAppend(struct ncclComm* comm, struct ncclInfo* info, struct ncclDevRedOpFull opDev) 将调用参数写入新建的集合任务节点,解析逐任务配置;普通集合路径将节点接入 collSorter,批量 Broadcast 分支另存到对应 root 的队列。
inline void ncclTaskCollSorterInsert(struct ncclTaskCollSorter* me, struct ncclTaskColl* x, size_t size) 根据 size 所在的流量区间确定任务分组,将任务插入该组的链表首部。
inline struct ncclTaskColl* ncclTaskCollSorterDequeueAll(struct ncclTaskCollSorter* me) 返回整个任务链表;链表非空时清空 sorter 自身状态,任务节点交由调用方继续使用。
ncclResult_t ncclGetAlgoInfo(struct ncclComm* comm, struct ncclTaskColl* task, int collNetSupport, int nvlsSupport, int numPipeOps, ncclSimInfo_t* simInfo = NULL) 根据聚合任务和能力输入选择算法、协议及资源参数,通过 task 写回结果;提供 simInfo 时写入所选算法与协议执行该聚合任务的预计集合通信耗时(微秒)。
ncclResult_t ncclGetRegBuff(struct ncclComm* comm, struct ncclTaskColl* info, int* regBuff) 根据任务描述的缓冲区范围查询注册记录,并结合 Graph 注册条件写入 regBuff;实际缓冲区注册由后续阶段处理。
ncclResult_t ncclTuningCompute(struct ncclTuningInput_t* const input, struct ncclTuningResult_t* const result) 按输入评估候选,存在 tuner 时接受插件调整,选择结果并计算资源参数,通过 result 输出;本文详细介绍多 rank 场景下的传统内核路径。
ncclResult_t ncclTuningGetChannels(struct ncclTuningInput_t* const input, struct ncclTuningResult_t* const result) 根据已选算法、协议、消息规模和 communicator 资源计算 Channel 与 warp 数,结合插件上限更新 result;具体 Channel 区间由后续编排确定。

algoNeedConnect 按算法索引,长度为 NCCL_NUM_ALGORITHMS;needConnect 汇总本次是否提出连接请求。这两个输出与函数返回的 ncclResult_t 分别表示连接需求和执行状态。ncclPrepareTasks 的连接需求登记规则见第 5 节。

2.3 任务与规划器

下图展示任务从登记到预处理完成时的数据结构关系:ncclKernelPlanner 持有跨阶段使用的任务队列,ncclPrepareTasks 中的局部数组、临时聚合任务和分类队列用于重组任务与查询调优结果。分类与聚合过程中,各原任务继续保留,变化的是链表链接和调优字段;图中同名节点表示同一任务在不同阶段的状态。

示例沿用第 2.1 节的 A/B/C/D,假设四个任务均留在传统路径,并最终选用 Ring,以便展示确定的队列顺序。

  1. 登记到排序器。 collTaskAppend 创建并填写 ncclTaskColl,通过 ncclTaskCollSorterInsert 将任务接入 planner->collSorter。按本例的流量估计与登记顺序,登记完成后的链表为 D → C → B → A。
  2. 重组类别链表。 ncclPrepareTasks 先调用 ncclTaskCollSorterDequeueAll,取得整个任务链表的首指针,同时清空 collSorter 的首尾指针和索引状态;原任务节点继续保留。本例均沿传统 AllReduce 路径处理。函数逐个遍历任务,根据集合操作类型、设备归约操作类型和数据类型 (func, opDev.op, datatype) 确定类别,将任务插入 tasksByFnOpTy 中对应类别链表的首部。本例得到 A → B → D 和 C 两条链表,fnOpTyIndices 记录已出现的类别索引;图中以 i、j 代指索引值。
  3. 构造临时聚合任务。 A、B 满足共享调优条件,函数将首任务复制到临时 agg,再累计两者的 count 和 trafficBytes;D 因隔离标记独立调优,C 在另一类别中独立调优。每个聚合区间分别构造 agg,原任务节点仍留在待处理链表中。
  4. 写回后按约束入队。 ncclGetAlgoInfo 更新 agg 的调优字段。调用方逐个处理原任务:先保存 next 指向的后继,写回调优结果,再通过 ncclIntruQueueEnqueue 将当前任务追加到局部队列 collBins[isCollnet][isNvls] 的尾部。入队会改写当前任务的 next,后续遍历使用此前保存的后继。此时 collSorter 早已在第 2 步取出整个链表时清空;分类队列接入的是原任务节点,临时 agg 本身不入队。本例假设均选 Ring,四个任务依次进入 [0][0] 队列。
  5. 拼接为后续输入。 ncclIntruQueueTransfer 按二维索引顺序,将四个分类队列中的任务转移到 planner->collTaskQueue,并清空来源队列。本例输出 A → B → D → C;后续注册与编排仍逐项使用这些任务,为其构造或复用 Work,再组织执行计划。

ncclTaskColl 表示一次已登记的集合通信操作,包含用户缓冲区引用、元素数量、归约描述和逐任务配置。预处理阶段重排任务链接并写入调优字段。

cpp 复制代码
struct ncclTaskColl {
  // 当前任务所在侵入式链表的后继。
  struct ncclTaskColl* next;
  // 集合操作类型。
  ncclFunc_t func;
  // 用户输入缓冲区引用。
  void const* sendbuff;
  // 用户输出缓冲区引用。
  void* recvbuff;
  // 以 datatype 为单位的元素数量。
  size_t count;
  // 有根集合操作使用的 rank。
  int root;
  // 任务元素类型。
  ncclDataType_t datatype;
  // 主机归约操作描述。
  ncclRedOp_t opHost;
  // 设备归约操作及其参数。
  struct ncclDevRedOpFull opDev;
  // 用于任务排序与后续调度的流量估计。
  size_t trafficBytes;
  // 调优给出的 Channel 数量上限。
  int32_t nMaxChannels:8;
  // 调优给出的 warp 数量。
  int32_t nWarps:8;
  // 选定的算法。
  int32_t algorithm:8;
  // 选定的协议。
  int32_t protocol:8;
  // 是否使用 CollNet 调度约束。
  uint32_t isCollnet:1;
  // 是否使用 NVLS 调度约束。
  uint32_t isNvls:1;
  // 设备函数标识。
  uint32_t devFuncId:29;
  // 是否需要独立调优。
  bool aggIsolate;
};

ncclKernelPlanner 是每个 communicator 持有的主机端任务与执行计划管理对象,通过 comm->planner 访问。它保存 Group 内登记的任务、任务计数和流上下文,并持有预处理后的分类队列,供后续注册与工作编排使用。编排过程中,它保存 Work 队列、当前 Plan 的构建状态和待启动的 Plan 队列,衔接任务登记、预处理、计划构建与内核启动。下面仅摘录与本文任务预处理相关的成员。

cpp 复制代码
struct ncclKernelPlanner {
  struct Peer {
    // 按 Broadcast root 索引的任务队列。
    struct ncclIntruQueue<struct ncclTaskBcast, &ncclTaskBcast::next> bcastQueue;
  };
  // 按流量规模近似排序的集合通信任务。
  struct ncclTaskCollSorter collSorter;
  // 长度为 nRanks 的 peer 数组。
  struct Peer* peers;
  // 传统集合通信任务数量。
  int nTasksColl;
  // 保留在 Broadcast 队列中的任务数量。
  int nTasksBcast;
  // Broadcast root 的范围和不同 root 的数量。
  struct {
    // 已登记 Broadcast 的最小 root。
    int minBcastPeer;
    // 已登记 Broadcast 的最大 root。
    int maxBcastPeer;
    // 已登记任务涉及的不同 root 数量。
    int BcastPeers;
  } bcast_info;
  // 当前任务是否属于有效的 CUDA Graph 捕获。
  bool persistent;
  // 当前捕获上下文。
  struct ncclCudaGraph capturingGraph;
  // 分类、聚合调优后供传统计划使用的任务队列。
  struct ncclIntruQueue<struct ncclTaskColl, &ncclTaskColl::next> collTaskQueue;
  // 被筛入对称 kernel 路径的任务队列。
  struct ncclIntruQueue<struct ncclTaskColl, &ncclTaskColl::next> collSymTaskQueue;
  // NVLS 分支提前构造的临时 Work 队列。
  struct ncclIntruQueue<struct ncclWorkList, &ncclWorkList::next> tmpCollWorkQueue;
  // 注册等操作关联的清理回调队列。
  struct ncclIntruQueue<struct ncclCommCallback, &ncclCommCallback::next> collCleanupQueue;
};

调优对象连接临时聚合任务与 communicator 的共享 Cost Model。下图区分两种作用域:ncclTuningContext_t 随 communicator 保留,input 与 bestTuning 则是一次 ncclGetAlgoInfo 调用中的局部对象。虚线表示字段传递或 Cost Model 参数读取;图中的 agg 与前图的临时聚合任务相同。

  1. 复用 Cost Model 状态。 comm->tuningContext 保存已初始化的候选启用状态、延迟、带宽和线程阈值。调优通过 input.comm 访问当前 communicator 的 Cost Model 状态。
  2. 形成单次输入。 ncclPrepareTasks 持有 agg,ncclGetAlgoInfo 将其操作、规模和配置转换为 ncclTuningInput_t,并补充注册状态和能力输入;input.comm 是引用,Cost Model 参数数组保留在 communicator 中。
  3. 接收并写回结果。 ncclTuningCompute 评估候选、按需调用插件并选择结果,再把算法、协议和资源参数写入 bestTuning。ncclGetAlgoInfo 根据该结果更新 agg,调用方随后将字段写回区间内的原任务,具体过程见第 4.2、4.3 节。

ncclTuningContext_t 是 ncclComm 的成员,通过 comm->tuningContext 访问。初始化阶段建立其中的 Cost Model 参数,后续调优复用这些状态;逐任务候选掩码保存在单次输入中。下面摘录与传统内核调优相关的成员。

cpp 复制代码
struct ncclTuningContext_t {
  // 调优常量,初始化时可由 tuner 插件调整。
  ncclTunerConstants_t tuningConstants;
  // 各集合操作是否由环境配置强制选择实现。
  int forced[NCCL_NUM_FUNCTIONS];
  // 各 tuning ID 对各集合操作的启用状态。
  int enabled[NCCL_TUNING_COUNT][NCCL_NUM_FUNCTIONS];
  // 按集合操作、算法和协议索引的基础延迟。
  float generalLatencies[NCCL_NUM_FUNCTIONS][NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
  // 按集合操作、算法和协议索引的基础带宽。
  float generalBandwidths[NCCL_NUM_FUNCTIONS][NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
  // 按算法和协议索引的线程工作量阈值。
  ssize_t threadThresholds[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
  // 按算法和协议索引的最大线程数。
  int maxThreads[NCCL_NUM_ALGORITHMS][NCCL_NUM_PROTOCOLS];
};

ncclTuningInput_t 保存一次调优的输入,comm 引用初始化阶段形成的拓扑与 Cost Model 参数。下面摘录 ncclGetAlgoInfo 调用点填写的字段;同一结构中供 symmetric/CE 路径使用的其他成员不在此列出。

cpp 复制代码
struct ncclTuningInput_t {
  // 所属 communicator,提供拓扑、Cost Model 状态和 tuner 上下文。
  struct ncclComm* comm;
  // 本次参与评估的实现集合,每一位对应一个 tuning ID。
  uint64_t tuningMask;
  // 集合操作类型。
  ncclFunc_t func;
  // 主机归约操作。
  ncclRedOp_t redOp;
  // 设备归约操作类型。
  ncclDevRedOp_t devRedOp;
  // 元素类型。
  ncclDataType_t datatype;
  // 本次调优的消息字节数。
  size_t nBytes;
  // 调用方估计的每 Channel 任务数。
  int numPipeOps;
  // 临时聚合任务中的元素数量。
  size_t count;
  // 用户缓冲区注册状态及 Graph 注册条件的查询结果。
  int regBuff;
  // 本操作类别的 CollNet 能力标记。
  int collNetSupport;
  // 本操作类别的 NVLS 能力标记。
  int nvlsSupport;
  // 已解析的逐任务 CTA 策略。
  int CTAPolicy;
};

ncclTuningResult_t 保存候选评估和资源计算的结果。

cpp 复制代码
struct ncclTuningResult_t {
  // 候选 ID。
  int id;
  // Cost Model 评估是否有效。
  int valid;
  // Cost Model 或插件给出的估算通信耗时,单位为微秒。
  float timeUs;
  // 可选的耗时比较值,单位为微秒;正值时优先用于选择。
  float selectionTimeUs;
  // 选定的算法。
  int algo;
  // 选定的协议。
  int proto;
  // 本次资源计算得到的 Channel 数。
  int nChannels;
  // 写回任务时使用的 Channel 数量约束。
  int maxChannels;
  // 每个 Block 的 warp 数。
  int nWarps;
};

2.4 任务排序结构

ncclTaskCollSorter 按流量区间对任务分组,用一条链表串联所有分组。每组任务在链表中连续存放,bins 按分组编号记录访问该组首任务的入口。入口保存的是首任务指针的地址,可能是 &head,也可能是前一任务的 next 字段地址。这种二级指针使插入操作可以直接修改指向该组首任务的链接。

cpp 复制代码
struct ncclTaskCollSorter {
  // 编码前将流量字节数换算为 KiB,并向下取整。
  static constexpr int UnitLog2 = 10;
  static constexpr size_t UnitSize = 1 << UnitLog2;
  // 用于确定流量分组的编码上限为 1 GiB。
  static constexpr int MaxLog2 = 30;
  static constexpr size_t MaxSize = 1ull << MaxLog2;
  // 流量编码保留的尾数位数。
  static constexpr int BitsPerPow2 = 2;
  // 每个较大二进制数量级划分为四个流量区间。
  static constexpr int BinsPerPow2 = 1 << BitsPerPow2;
  // 流量分组索引,共 81 项。
  static constexpr int BinCount = 1 + (MaxLog2 - UnitLog2) * BinsPerPow2;
  // 全部任务组成的链表首节点。
  struct ncclTaskColl* head;
  // 全部任务组成的链表尾节点。
  struct ncclTaskColl* tail;
  // 此编号及后续编号对应的分组均无任务。
  int binEdge;
  // 每项保存指向该组首任务的指针地址。
  struct ncclTaskColl** bins[BinCount];
};

由于 bins 可能指向 sorter 自身的 head,该结构包含自引用地址,源码明确要求不能通过 memcpy 拷贝。第 3.2 节用具体流量值和分组索引说明它如何组织任务。

3. 任务登记与排序

collTaskAppend 把一次集合通信调用记录为主机任务节点,并通过 ncclTaskCollSorterInsert 将普通集合任务接入 planner->collSorter。登记阶段填写操作参数和任务约束,排序器按流量区间组织待处理的链表;第 4 节的 ncclPrepareTasks 再取出这些节点进行分类与聚合。

3.1 调用参数登记

以普通 AllReduce 路径为例,collTaskAppend 在将 communicator 接入当前 Group、记录 stream 的捕获上下文后,依次完成以下工作。

  1. 取得任务节点。 ncclMemoryPoolAlloc<ncclTaskColl> 从 comm->memPool_ncclTaskColl 复用空闲节点,或向 comm->memPermanent 申请空间,再用 memset 清零。底层 ncclMemoryStack::allocate 按对齐要求推进当前内存块的分配位置;空间不足时由 allocateSpilled 复用其他内存块,或调用 malloc 扩充主机存储。
  2. 填写任务描述。 将 info 中的集合类型、缓冲区引用、元素数量、root 和数据类型写入节点,同时保存主机归约操作 opHost 和已转换的设备描述 opDev。AllReduce 使用 trafficBytes = count × ncclTypeSize(datatype) × 2 估计流量。
  3. 保存配置并接入排序器。 根据单次调用配置生成 aggIsolate,解析 CTA 资源边界和算法约束,保存事件及 profiler 信息;随后增加 planner->nTasksColl,调用 ncclTaskCollSorterInsert(&planner->collSorter, t, t->trafficBytes) 进行排序。

ncclTypeSize(datatype) 返回每个元素的字节数;AllReduce 的系数 2 来自 ncclFuncTrafficPerByte,用于统一估计调度规模。以 Ring 为例,设 P 个 rank、单个输入缓冲区大小为 S,在均分且忽略填充和协议开销的假设下,每个 rank 在 ReduceScatter 与 AllGather 阶段合计发送 2 × (P - 1) / P × S 字节,P 较大时接近 2 × S。

满足批量 Broadcast 优化条件的调用另建 ncclTaskBcast,追加到 planner->peers[root].bcastQueue。

3.2 流量区间排序

ncclTaskCollSorterInsert 根据 trafficBytes 确定流量区间,再将任务插到该区间对应链表段的首部。所有区间共用一条链表,较大流量区间排在前面,同一区间内按登记顺序反向排列。因此,排序器给出近似顺序;操作类别和共享调优范围由第 4 节确定。

  1. 确定区间。 排序器先限制编码输入的大小,再按二进制数量级确定流量分组。下面的上下限公式用于解释 u32fpEncode 的分组规则;源码直接计算编码,不单独保存区间边界。

    1. 截断并换算单位。 计算 x = min(MaxSize, size) >> UnitLog2,其中 MaxSize=1 GiB、UnitLog2=10。min 将参与编码的流量限制在 1 GiB 以内,右移将字节数向下取整换算为整数 KiB;原任务字段仍保存实际流量。
    2. 计算区间宽度。 当 x >= 4 时,令 k = floor(log2(x)),则 x 位于 [2^k, 2^(k+1)) 这一数量级。BitsPerPow2=2 将该数量级等分为四段,每段宽度为 Δ = 2^(k-2) KiB;当 x < 4 时,每段宽度为 1 KiB。
    3. 确定上下限。 对未达到 1 GiB 的流量,区间下限为 L = floor(x / Δ) × Δ,上限为 U = L + Δ,两者的单位均为 KiB。区间采用左闭右开的形式 [L, U):流量达到 U 时进入下一组。原始流量达到或超过 1 GiB 时,编码输入均被截断为 1 GiB,共用对应原始流量范围 [1 GiB, +∞) 的饱和分组。
    4. 编码并反转编号。 u32fpEncode(x, 2) 返回所属区间的 code,再用 bin = BinCount - 1 - code 得到分组索引。这里 BinCount=81,因此 bin=80-code;反转编号使较大流量区间排在前面,区间边界保持不变。
  2. 维护分组入口。 区间首次加入任务时,排序器记录该组入口,并按需更新后继分组的入口地址和链表尾指针。已有任务的区间再次插入时,复用原入口。

  3. 接入原节点。 执行 x->next = *me->bins[bin] 和 *me->bins[bin] = x,把任务接到该组首部。bins[bin] 保存入口指针的地址,可能指向 head,也可能指向前一任务的 next 字段。

沿用第 2.3 节的 A/B/C/D 任务,四个任务依次登记,流量估计分别为 12、24、24、36 KiB。A 所在数量级为 [8, 16) KiB,区间宽度为 2 KiB;B、C 所在数量级为 [16, 32) KiB,区间宽度为 4 KiB;D 所在数量级为 [32, 64) KiB,区间宽度为 8 KiB。各任务的流量区间、编码与插入结果如下。

登记任务 流量估计 所属流量区间 code 分组索引 插入后的完整链表
A 12 KiB [12, 14) KiB 10 70 A
B 24 KiB [24, 28) KiB 14 66 B → A
C 24 KiB [24, 28) KiB 14 66 C → B → A
D 36 KiB [32, 40) KiB 16 64 D → C → B → A

下图展示登记完成后的分组入口、指针存储位置和完整任务链表。B、C 虽然数据类型不同,仍因流量估计相同进入同一排序分组;数据类型在第 4.1 节的操作分类中才用于区分类别。

  1. 链表顺序。 head 指向 D,之后依次为 C、B、A,tail 指向 A。C 后于 B 登记,在分组 66 中排到 B 前面。
  2. 分组入口。 bins[64] 保存 &head,bins[66] 保存 &D.next,bins[70] 保存 &B.next,分别访问 D、C、A。C、B 在同一组内连续排列,C.next 连接 B。
  3. 稀疏索引。 本例只有 64、66、70 三个非空分组,其余 bins 项为空,binEdge=71。新建分组且后面已有任务时,插入函数从 bin+1 向后跳过空项,找到第一个非空分组,再调整入口引用;预处理时直接沿完整链表遍历。

4. 任务分类、聚合与调优

ncclPrepareTasks 将排序后的任务按操作属性分类,再构造共享调优结果的聚合区间,最终生成供后续调度使用的任务队列。本节沿用 A/B/C/D,分析传统 AllReduce 路径;输入检查完成后的主要处理分为四步,对应第 2.3 节的步骤 2---5。

  1. 任务分类。 按 (func, opDev.op, datatype) 将任务重新分类。本例从 D → C → B → A 得到 Float32 的 A → B → D 和 Float64 的 C,见第 4.1 节。
  2. 调优。 在各类别中,先确定 CollNet/NVLS 能力标记并估计每个 Channel 的任务数量,再按流量阈值和隔离标记划分聚合区间,累计规模后调用 ncclGetAlgoInfo。本例形成 A/B、D、C 三个区间,见第 4.2 节。
  3. 结果写回。 将当前区间的调优结果写回各原任务,再按 isCollnet、isNvls 追加到 collBins,见第 4.3 节。
  4. 输出任务队列。 全部类别处理结束后,按二维索引顺序把 collBins 拼接到 planner->collTaskQueue。本例假设均选 Ring,输出 A → B → D → C,见第 4.4 节。

步骤 2、3 在每个聚合区间内连续执行;所有类别处理完成后,才执行步骤 4。函数随后记录连接需求,这部分见第 5 节。

4.1 任务分类

tasksByFnOpTy 是局部首指针数组,使用前由 memset 清零,每个有效入口指向一个类别的任务链表。取出和分类的流程如下:

  1. 保存后继。 读取当前 task->next 并存入局部 next,避免改接链接后丢失原链表的遍历位置。

  2. 确定类别。 按 (func, opDev.op, datatype) 三元组分类,三个字段全部相同的任务进入同一条类别链表。

    1. 集合通信操作类型 func。 区分 AllReduce、AllGather、ReduceScatter 等操作;Ring、Tree 是后续调优选择的算法。
    2. 设备归约操作类型 opDev.op。 使用用户归约操作转换后的设备操作类型,如 ncclDevSum、ncclDevProd;归约标量等参数仍由各任务保存。
    3. 数据类型 datatype。 区分 ncclFloat32、ncclFloat64 等元素类型。

    公式 ((func * ncclNumDevRedOps + opDev.op) * ncclNumTypes + datatype) 将三元组展开为一维数组索引。该类别首次出现时,把索引追加到 fnOpTyIndices,供后续按首次出现顺序遍历。

  3. 插入类别链表首部。 将 task->next 改为该类别原有的首指针,再把 tasksByFnOpTy[index] 改为当前任务,最后从此前保存的 next 继续遍历。因此,每个类别内的原遍历顺序反转,较小流量区间优先。

本例中 A、B、D 的分类键均为 AllReduce + ncclDevSum + ncclFloat32,因此归入同类;C 的操作类型和设备归约操作相同,但数据类型为 ncclFloat64,进入另一类。从 D → C → B → A 开始遍历时,依次遇到这两个类别:Float32 链表依次形成 D、B → D、A → B → D,Float64 链表只有 C。

4.2 调优

ncclPrepareTasks 先为当前任务类别准备能力标记和任务数估计,再逐个构造聚合区间,调用 ncclGetAlgoInfo 选择算法、协议和资源数量。下面依次分析参数准备、同类任务聚合,以及 ncclGetAlgoInfo 对算法与执行参数的选择。

4.2.1 准备调优参数

进入一个任务类别时,ncclPrepareTasks 先用该类别的首任务查询 CollNet/NVLS 能力,再估计每个 Channel 的任务数量。这些参数在开始任务聚合前确定。

cpp 复制代码
struct ncclTaskColl* aggBeg = tasksByFnOpTy[fnOpTyIndices[cursor]];
int collNetSupport = 0;
NCCLCHECK(ncclGetCollNetSupport(comm, aggBeg, &collNetSupport));
int nvlsTransportEnabled =
  ncclNvlsTransportEnabled(comm) &&
  (ncclNvlsSupported(aggBeg->opDev.op, aggBeg->datatype) ||
   aggBeg->func == ncclFuncAllGather);
int nTasksPerChannel = divUp(comm->planner.nTasksColl, comm->nChannels);
  1. 查询 CollNet 能力。 对本文的 AllReduce,ncclGetCollNetSupport 结合 comm->config.collnetEnable 与操作、数据类型对应的能力矩阵项设置 collNetSupport。其中 ncclDevPreMulSum、ncclDevSumPostDiv 按 ncclSum 查询。
  2. 判断 NVLS 能力。 nvlsTransportEnabled 要求 NVLS transport 已启用,且当前设备归约操作与数据类型受到支持;共用代码还允许 AllGather 通过后一项判断。该值随后作为 nvlsSupport 传入调优函数,供候选组合的 Cost Model 检查。
  3. 估计每 Channel 的任务数。 nTasksPerChannel 等于传统任务总数 planner->nTasksColl 除以 comm->nChannels 后向上取整。它使用当前 communicator 的 Channel 数,并非某个聚合区间的任务数;此时算法尚未选定,NVLS 也沿用这一粗略估计。

能力标记用于后续筛选算法与协议组合,任务数估计参与通信的耗时计算。参数准备完成后,函数进入 do 循环,为当前类别逐个构造聚合区间。

4.2.2 聚合同类任务

每轮聚合从 aggBeg 开始,先将首任务复制到临时 agg,再累计满足条件的相邻任务的 count 和 trafficBytes。aggEnd 指向尚未纳入区间的第一个任务,聚合范围为 [aggBeg, aggEnd);各原任务继续保存自己的元素数量和缓冲区引用。下面是决定聚合范围的源码片段。

cpp 复制代码
struct ncclTaskColl* aggEnd = aggBeg->next;
struct ncclTaskColl agg = *aggBeg;
while (aggEnd != nullptr &&
       aggEnd->trafficBytes < 4 * aggBeg->trafficBytes &&
       !aggBeg->aggIsolate && !aggEnd->aggIsolate) {
  agg.count += aggEnd->count;
  agg.trafficBytes += aggEnd->trafficBytes;
  aggEnd = aggEnd->next;
}

比较基准始终是区间首任务的 trafficBytes。候选任务恰好达到该基准的 4 倍时,当前区间结束;阈值也不随 agg.trafficBytes 的累加而增大。由于输入只是近似排序,函数沿类别链表逐个检查相邻任务,满足条件就继续累计,遇到首个不满足条件的任务便结束当前区间。

aggIsolate 标记任务是否独立成组,若是独立任务,则单独调优,不聚合。collTaskAppend 在登记时根据单次调用配置生成该标记。

下图继续使用第 2.3 节的 A/B/C/D 四个任务,展示如何从两条类别链表中构造三个用于调优的临时聚合任务。Float32 类别先处理 A/B,再处理隔离任务 D;随后处理 Float64 类别中的 C。

  1. A/B 共享调优。 A 的流量估计为 12 KiB,B 为 24 KiB,满足 24 < 4 × 12,且两者均未隔离。D 虽然也满足流量条件,却因 aggIsolate=true 结束当前区间。A/B 的临时 agg.count=4608,聚合流量为 36 KiB。
  2. D 独立调优。 D 显式设置 maxCTAs=4,独立构造 agg,其中 count=4608,流量为 36 KiB。它与 A/B 聚合后的规模相同,但保留自己的资源约束。
  3. C 在另一类别调优。 C 使用 Float64,独立构造 agg,其中 count=1536,流量为 24 KiB。最终有三次调优,仍保留四个原任务节点。

区间确定后,调用方把临时 agg 与第 4.2.1 节准备的参数传给调优函数:

cpp 复制代码
NCCLCHECK(ncclGetAlgoInfo(comm, &agg, collNetSupport,
                         nvlsTransportEnabled, nTasksPerChannel, simInfo));
4.2.3 选择算法与执行参数

ncclGetAlgoInfo 根据临时聚合任务的规模、配置和 communicator 能力选择算法与协议,计算 Channel 上限和 warp 数,并把结果写回 agg。执行流程如下:

  1. 准备调优输入。 计算消息大小,初始化算法与协议字段,确定候选掩码,填写操作属性、能力标记和任务数估计。
  2. 查询缓冲区注册状态。 查询已有注册记录及 Graph 注册条件,将结果写入 input.regBuff。
  3. 执行调优计算。 调用 ncclTuningCompute,评估算法与协议组合,同时接受插件调整,选择最优组合并计算资源参数。
  4. 扩大候选范围并重新调优。 按任务指定的候选范围进行首次调优时,若未选出可用算法,函数取消这一范围限制,在完整的传统内核候选范围内重新调优;环境配置、能力和 Cost Model 限制仍然生效。重算成功后,若 forceAlgSelection=0,则采用重算结果;否则因无法满足原候选限制而返回错误。
  5. 返回调优结果。 更新 agg 的算法、协议、Channel 上限和 warp 数;调用方提供 simInfo 时,同时写入所选算法与协议执行当前聚合任务的预计集合通信耗时,最后返回执行状态。
4.2.3.1 准备调优输入

ncclGetAlgoInfo 先根据元素类型和集合操作计算 nBytes,把临时聚合任务的 algorithm、protocol 置为未定义,再填写局部 ncclTuningInput_t input。

  1. 计算消息大小。 ncclFuncMaxSendRecvCount 根据集合操作类型确定发送与接收两侧较大的元素数量:AllGather 返回 nRanks × count,对应接收端汇总的元素数量;ReduceScatter 返回 nRanks × count,对应发送端参与归约的元素数量;其他操作返回 count。随后乘以 ncclTypeSize(info->datatype),得到用于调优的消息字节数 nBytes。本例 AllReduce 的 A/B、D、C 三次调用依次为 18 KiB、18 KiB、12 KiB,均按聚合后的 count × 元素大小 计算;排序与聚合使用的 trafficBytes 另行保留。

  2. 确定算法候选范围。 algMask 保存任务指定的算法候选范围,tuningMask 保存本次调优实际遍历的算法候选范围。两者使用相同的 64 位位图编码,环境配置与 Cost Model 的支持范围还会进一步限制可用候选算法。

    1. 按算法与协议组合确定候选位号。 algo 是算法编号,proto 是协议编号;每个组合对应的位图位号为 i = algo × 3 + proto。位号从最低位的 0 开始,1ULL << i 表示该位的掩码,置 1 表示将该组合纳入候选范围。下表行标题括号内为算法编号,列标题括号内为协议编号,单元格中的数字为组合对应的位图位号。例如,Ring 的算法编号为 1,Simple 的协议编号为 2,对应位号为 1 × 3 + 2 = 5。NCCL_TUNING_MASK_GENERAL_KERNELS=(1ULL << 21)-1=0x1FFFFF,覆盖位 0~20;带"未实现"的位保留在编号空间中,但当前没有对应的可用组合。

      算法(algo) LL(proto=0) LL128(proto=1) Simple(proto=2) 支持的集合通信操作
      Tree(0) 0 1 2 AllReduce
      Ring(1) 3 4 5 AllReduce、Broadcast、Reduce、AllGather、ReduceScatter
      CollNetDirect(2) 6(未实现) 7(未实现) 8 AllReduce、AllGather、ReduceScatter
      CollNetChain(3) 9(未实现) 10(未实现) 11 AllReduce
      NVLS(4) 12(未实现) 13(未实现) 14 AllReduce、AllGather、ReduceScatter
      NVLSTree(5) 15(未实现) 16(未实现) 17 AllReduce
      PAT(6) 18(未实现) 19(未实现) 20 AllGather、ReduceScatter
    2. 高位编码对称 kernel 与 CE 实现。 位 21~38 按 21 + symKernelId 编码对称 kernel,位 39~40 按 39 + ceMethodId 编码 Copy Engine(CE)方法,位 41~63 在当前版本中未分配。NCCL_TUNING_MASK_SYM_KERNELS 覆盖位 21~38,NCCL_TUNING_MASK_CE 覆盖位 39~40,NCCL_TUNING_MASK_ALL 覆盖位 0~40。下表列出对应的完整枚举名称。对称 kernel 由 GPU 的 SM 执行;CE 实现通过复制引擎完成通信,使用 cudaMemcpyAsync、cudaMemcpyBatchAsync 等接口提交复制操作,不是 SM 上执行的集合通信 kernel。

      位号 对应 kernel 或 CE 实现 实现方式
      21 ncclSymkKernelId_AllReduce_AGxLL_R LL:低延迟实现。
      22 ncclSymkKernelId_AllReduce_AGxLLMC_R LLMC:低延迟实现的多播变体。
      23 ncclSymkKernelId_AllReduce_RSxTmaLD_AGxTmaST TmaLD、TmaST:使用 TMA 的 Load、Store 实现。
      24 ncclSymkKernelId_AllReduce_RSxLD_AGxST LD、ST:基于 Load、Store 的实现。
      25 ncclSymkKernelId_AllReduce_RSxLDMC_AGxSTMC LDMC、STMC:基于 Load、Store 的实现,使用多播相关机制。
      26 ncclSymkKernelId_AllGather_LL LL:低延迟实现。
      27 ncclSymkKernelId_AllGather_LLMC LLMC:低延迟实现的多播变体。
      28 ncclSymkKernelId_AllGather_TmaST TmaST:使用 TMA 的 Store 实现。
      29 ncclSymkKernelId_AllGather_ST ST:基于 Store 的实现。
      30 ncclSymkKernelId_AllGather_TmaSTMC TmaSTMC:使用 TMA 的 Store 实现的多播变体。
      31 ncclSymkKernelId_AllGather_STMC STMC:基于 Store 的实现的多播变体。
      32 ncclSymkKernelId_AllGather_RailRing_LsaSTMC RailRing:跨节点环形通信;LsaSTMC:本地 Store 实现的多播变体。
      33 ncclSymkKernelId_ReduceScatter_LL LL:低延迟实现。
      34 ncclSymkKernelId_ReduceScatter_TmaLD TmaLD:使用 TMA 的 Load 实现。
      35 ncclSymkKernelId_ReduceScatter_LD LD:基于 Load 的实现。
      36 ncclSymkKernelId_ReduceScatter_LDMC LDMC:基于 Load 的实现,使用多播相关机制。
      37 ncclSymkKernelId_ReduceScatter_RailA2A_LsaLD RailA2A:跨节点全互连通信;LsaLD:基于本地 Load 的实现。
      38 ncclSymkKernelId_ReduceScatter_RailA2A_LsaLDMC RailA2A:跨节点全互连通信;LsaLDMC:基于本地 Load 的实现,使用多播相关机制。
      39 ncclCeMethodId_AllGather_UC(单播) UC:基于 CE 的单播实现。
      40 ncclCeMethodId_AllGather_MC(多播) MC:基于 CE 的多播实现。
    3. 从单次调用配置生成 algMask。 任务登记时,ncclCollConfigGetAlgMask 将 algSelection 解析为位图,再与当前集合操作支持的候选集合取交集。例如,RING 对应位 3、4、5,RING_SIMPLE 只对应位 5。当前名称注册表包含已实现的传统组合与对称 kernel,不包含 CE 实现。未指定选择时,algMask=0 表示自动选择;若配置无法解析,或交集为空,则 forceAlgSelection!=0 时直接返回错误,forceAlgSelection=0 时保留零掩码,允许自动选择。这一步发生在 ncclGetAlgoInfo 之前,因此环境选择不会跳过任务配置的合法性检查。

    4. 环境变量。 直接限定候选名称的环境变量有三个,均支持按集合操作指定列表,例如 NCCL_ALGO="allreduce:Tree,Ring"。

      环境变量 选择对象及作用
      NCCL_ALGO 传统算法:Tree、Ring、CollNetDirect、CollNetChain、NVLS、NVLSTree、PAT。
      NCCL_PROTO 传统协议:LL、LL128、Simple;与算法列表共同限定传统组合。
      NCCL_SYM_KERNEL 具体对称 kernel,使用配置名称,例如 AllReduce_AGxLL_R;该名称对应枚举 ncclSymkKernelId_AllReduce_AGxLL_R。

      三者分别设置算法、协议和对称 kernel 列表。传统组合须同时满足算法与协议列表;对称 kernel 使用自己的列表。只要 NCCL_ALGO 或 NCCL_SYM_KERNEL 中至少一个为非空字符串,初始化时就先清空传统算法与对称 kernel 列表,再按各自配置启用,因此只设置 NCCL_SYM_KERNEL 时,传统候选可能全部被禁用。

    5. 按"环境变量 → 任务选择 → 默认范围"确定 tuningMask。 优先采用对当前集合操作生效的环境变量配置;没有环境选择时,采用任务通过 algSelection 指定的候选范围;两者均未指定时,使用完整的传统 kernel 候选范围。所有候选仍须满足环境配置、拓扑和能力要求。

    6. 候选可用条件与回退规则。 候选算法是否可用,取决于集合操作、拓扑、运行时能力及相关配置,包括 NCCL_COLLNET_ENABLE、NCCL_NVLS_ENABLE 等能力开关。任务指定的候选范围内没有可用算法时,forceAlgSelection=0 允许回退到满足环境与能力约束的其他传统 kernel 候选;非零则返回错误。该配置不改变环境选择优先于任务选择的顺序,具体分支见第 4.2.3.4 节。

  3. 补充操作与能力信息。 调优输入 input 包含聚合任务 agg 的集合操作、主机与设备归约操作、数据类型、元素数量和 CTA 策略,并引用当前 communicator。collNetSupport、nvlsSupport 分别表示 CollNet、NVLS 的支持状态;numPipeOps 表示每个 Channel 的任务数量估计值。这三个参数均来自第 4.2.1 节的参数准备。

4.2.3.2 查询缓冲区注册状态

ncclGetAlgoInfo 通过 ncclGetRegBuff 查询缓冲区注册条件,将结果写入 input.regBuff,供后续调优使用。

  1. 查询目的。 已注册的用户缓冲区可能支持 GPU 直接访问、RDMA 或 NVLS 通信,减少中间缓冲区搬运,影响估算通信耗时和 Channel 需求。regBuff 会传给 tuner 插件,也用于满足条件时的 AllGather、ReduceScatter NVLS 效率策略选择。
  2. 查询范围与判定条件。 查询使用 agg 保留的首任务缓冲区地址,并按聚合后的元素数量计算发送与接收范围。两端范围均有有效的用户注册记录,或者当前 CUDA Graph 捕获有效且启用了 Graph 注册时,regBuff 为真。该值表示调优所依据的注册条件,不保证各项底层注册已经完成。
  3. NCCL 层的注册位置。 用户通过 ncclCommRegister 登记缓冲区后,当前 communicator 的 comm->regCache 保存对应地址范围及引用信息。ncclGetRegBuff 查询的就是这些记录,要求记录覆盖本次发送、接收范围,并具有有效的用户注册引用;此处只查询,不执行注册。
  4. 底层注册按通信路径完成。 后续阶段为各原任务按需建立底层资源:节点内 GPU 直接通信使用 CUDA IPC/VMM 映射;NET/RDMA 使用网络插件注册对象,RDMA 实现通常对应 NIC 可访问的 MR;CollNet 使用其插件注册对象;NVLS 使用 CUDA multicast 对象及地址映射。同一缓冲区可关联多种底层资源。
  5. 用户注册并非普通 AllReduce 的前提。 未预先调用 ncclCommRegister 时,NCCL 可以使用普通缓冲区路径;符合条件的 CUDA Graph 路径还可自动注册。这里的普通缓冲区注册与 ncclCommWindowRegister 建立对称窗口属于不同机制。
4.2.3.3 执行调优计算

下图展开多 rank、传统内核 AllReduce 的调优计算。子调用按从上到下的顺序执行;插件分支仅在已加载 tuner 时进入。图中展开 Ring、Tree Cost Model,省略分发接口 ncclTuningComputeTuning。

  1. 评估算法与协议组合。 按 Tuning ID 确定对应的算法与协议组合,检查配置、能力和 Cost Model 支持,计算有效组合的估算耗时,形成临时候选链表。
  2. 调用调优插件。 已加载 tuner 插件时,由插件调整候选组合的估算通信耗时和 Channel 上限,为后续选择提供依据。
  3. 选择最优组合。 ncclTuningSelectBestTuning 比较各候选组合的估算通信耗时,选择耗时最短的算法与协议组合。
  4. 计算资源并清理。 根据选定组合和消息规模计算 Channel、warp 数,将结果复制给调用方,再释放临时候选链表。

单 rank 时,ncclTuningCompute 直接选择 Ring/Simple,无需逐一评估其他算法与协议组合,也不调用调优插件;随后仍计算适用的资源参数。以下重点分析多 rank 分支。

  1. 筛选可用的算法与协议组合。

    传统 kernel 路径以算法与协议组合作为调优对象,每个组合的 Tuning ID 与位图位号一一对应。例如,Tree/LL、Tree/LL128、Tree/Simple 分别对应 ID 0、1、2,完整映射见第 4.2.3.1 节。

    ncclTuningComputeAllTunings 根据掩码、配置和运行时能力,筛选 Cost Model 支持的算法与协议组合。通过检查且估算通信耗时大于零的组合保存在候选链表中,供后续比较和选择。AllReduce 的主要筛选条件如下。

    候选范围 当前 AllReduce 路径的关键条件
    Ring、Tree 的 LL/LL128/Simple Cost Model 初始化后启用 AllReduce 组合;LL128 的默认启用还检查 GPU 能力、链路类型和驱动等条件。显式协议配置会影响默认筛选,具体规则见 isLL128Enabled 的调用点。
    CollNet Direct/Chain Cost Model 表只提供 Simple,实现检查 collNetSupport、有效带宽和本地 rank 数等条件。
    NVLS、NVLS_TREE Cost Model 表只提供 Simple,实现检查 nvlsSupport 和有效带宽;多节点 NVLS 还需要 collNetSupport。
    PAT 当前 Cost Model 表为 AllGather/ReduceScatter 启用,AllReduce 不进入其候选。
    symmetric kernel、CE 本调用的传统内核掩码将它们排除;共用调优引擎对其他调用者支持的实现不等于本路径候选。
  2. 估算通信耗时。

    Ring 和 Tree 分别使用 NCCL 内置的 Ring Cost Model 和 Tree Cost Model。两者都按"延迟项+数据传输项"估算集合通信耗时,区别在于通信阶段、有效带宽和修正系数的计算方式。以下先说明 AllReduce 的计算规则,再用 A/B 聚合任务代入数值;忽略调优插件和自定义规则。

    1. 确定 Cost Model。 modelMap 按 Tuning ID 选择对应的初始化函数和估算函数。初始化函数根据 communicator 的拓扑与协议保存 generalLatencies、generalBandwidths;估算函数再结合本次消息大小计算 timeUs。函数名中的 Sim 表示执行模型计算,本次调用不会启动 GPU 通信来测量时间。

      算法与协议 Tuning ID 初始化函数 估算函数
      Tree / LL、LL128、Simple 0、1、2 ncclTuningTreeModelInit ncclTuningTreeModelSim
      Ring / LL、LL128、Simple 3、4、5 ncclTuningRingModelInit ncclTuningRingModelSim

      两种 Cost Model 都含有协议效率、带宽上限等经验参数。它们在主机上计算预计集合通信耗时,供算法选择使用;该值不表示 ncclGetAlgoInfo 自身的执行耗时。分发与默认常量见 src/tuning/cost_model.cc。

    2. 使用统一公式计算通信耗时。 Ring、Tree 分别计算各自的延迟与有效带宽,再代入公共函数 ncclTuningGetTime 的统一公式。记 S=input.nBytes,单位为 byte;L 为对应 Cost Model 修正后的延迟,单位为 µs;B 为对应 Cost Model 修正后的算法有效带宽,单位为 GB/s。对本文 Float32 任务,计算如下:

      text 复制代码
      T = L × q + S / (1000 × B)                         // µs
      
      Ring: L = L_Ring, B = B_Ring, q = numPipeOps
      Tree: L = L_Tree, B = B_Tree, q = ceil(numPipeOps / NCCL_MAX_DEV_WORK_BATCH_COLLS)

      T 是预计集合通信总耗时,包含延迟项 L×q 和数据传输项 S/(1000×B),单位为 µs。后文分别说明 L_Ring、B_Ring 和 L_Tree、B_Tree 的计算方法。

      q 表示模型累计延迟的次数。S 使用聚合任务的消息字节数;Ring 的重复传输和 Tree 的归约、广播已折算进 B,代入公式时不再给 S 乘通信量系数。numPipeOps 表示每 Channel 的集合通信任务数估计值。NCCL_MAX_DEV_WORK_BATCH_COLLS 表示一批最多可容纳多少个 ncclDevWorkColl 工作描述。1000 用于把 byte 除以 GB/s 的结果换算成 µs。实现见 src/tuning/tuning_general.cc。

    3. 计算 Ring 的延迟与有效带宽。 AllReduce 沿环执行 ReduceScatter 和 AllGather。对 P=nRanks 个 rank,每个阶段有 P−1 步,共 K=2×(P−1) 步;均分时每步发送 S/P,每 rank 总发送量为 S×K/P。

      设 C=graphs[Ring].nChannels,b 为 Cost Model 从拓扑图中选取的每 Channel 带宽,先得到 B0=b×C。单节点,或节点数不超过 2 且 GPU 计算能力小于 100 时,使用 bwIntra;其余情况使用 bwInter。协议按下表把 B0 修正为 Bbus,再折算为算法有效带宽:

      Ring 协议 Bbus 的计算
      LL min(llMaxBw, B0 × 0.5)
      LL128 min(B0 × 0.92, C × perChMaxRingLL128Bw)
      Simple B0
      text 复制代码
      B_Ring = Bbus × P / K
      
      单节点:L_Ring = Lbase + K × Lintra
      多节点:L_Ring = Lbase + (K − Kinter) × Llocal + Kinter × Linter
               Kinter = 2 × (nNodes − 1)

      Lbase 是算法与协议对应的基础延迟,Lintra 是按 NVLink 或 PCIe 类别查得的节点内单步延迟。多节点的 Llocal 还取网络提交开销与 Lintra 的较大值,Linter 包含网络延迟及拓扑图的附加延迟。对单节点,数据传输项等价于 S×K/P ÷ Bbus,因此环上的重复传输只计入一次。

      ncclTuningRingModelSim 还会在多节点 AllReduce、单节点 Blackwell NVLink 等条件下修正 Simple 的延迟;后面的单节点 PCIe 示例不触发该分支。实现见 src/tuning/ring.cc。

    4. 计算 Tree 的延迟与有效带宽。 Tree 先向上归约,再向下广播。模型分别计算节点内链与节点间树的深度,用往返两遍的路径估算延迟:

      text 复制代码
      Dlocal = nRanks / nNodes − 1
      Dinter = floor(log2(nNodes))
      L_Tree = Lbase + 2 × (Dlocal × Lintra + Dinter × Linter)

      nRanks 为 rank 总数,nNodes 为节点数。Dlocal 表示节点内链的估计深度,按每节点平均 rank 数减一计算,其中 nRanks / nNodes 使用整数除法;Dinter 表示节点间树的估计深度,取节点数以 2 为底的对数并向下取整。节点内链的初始化见 src/graph/connect.cc 的 ncclTopoPreset。

      Lbase 是算法与协议对应的基础延迟,Lintra、Linter 分别为模型采用的节点内、节点间单步延迟;系数 2 表示向上归约和向下广播两个方向的延迟累计。

      带宽同样从 B0=b×C 出发,其中 C=graphs[Tree].nChannels。计算能力小于 100 时,至多两个节点使用 bwIntra,更多节点使用 bwInter;计算能力至少为 100 时,取两者较小值。AllReduce 先执行 B1=min(B0×0.92, C×perChMaxTreeBw),再按协议修正:

      Tree 协议 协议修正后的 B2
      LL min(B1 / 3.8, llMaxBw)
      LL128 min(B1 × r, C × perChMaxTreeLL128Bw);单节点 r=7/9,多节点 r=120/128
      Simple B1

      若 maxTreePattern==NCCL_TOPO_PATTERN_TREE,再将 B2 乘以 0.85。随后乘以 0.5,把归约、广播两个方向折算为算法有效带宽。每次估算时,还按消息大小查 treeCorrectionFactor:

      text 复制代码
      Bstored = B2 × patternFactor × 0.5
      k = floor(log2(S >> 6))
      B_Tree = Bstored × treeCorrectionFactor[proto][k]  // 0 ≤ k < 23

      patternFactor 在上述 Tree pattern 条件下为 0.85,否则为 1;k 不在查表范围内时,直接使用 Bstored。树深度影响延迟项,带宽项采用包含流水传输效果的经验模型,因此不会再按链深度把整条消息的传输时间乘三次。实现见 src/tuning/tree.cc;修正表见 src/tuning/cost_model.cc。

    5. 设置 A/B 聚合任务的示例参数。 以下仅比较 Ring / Simple 与 Tree / Simple,采用单节点四块 Hopper GPU,每 GPU 一个 rank,四块 GPU 分别通过 PCIe 5.0 x16 链路连接到同一个 PCIe switch。假设 P2P 路径可用,并采用下表中的拓扑图参数。24 GB/s 是为演算设置的模型输入,不是 PCIe 5.0 的标称带宽或实测结果;实际分析应读取 communicator 已生成的拓扑图。

      参数 示例取值与来源
      每 rank 的 A/B 数据 A:1536×4=6144 byte;B:3072×4=12288 byte
      聚合消息大小 S (1536+3072)×4=18432 byte=18 KiB;第 3 节用于排序的聚合流量为 36 KiB,本处使用消息大小 18 KiB
      节点与 rank 数 nNodes=1,nRanks=4,无 NVLink 路径,延迟常量使用 NCCL_HW_PCI 类别
      拓扑带宽与 Channel 数 假设 Ring、Tree 均为 bwIntra=24 GB/s、graphs[algo].nChannels=2,因此 B0=48 GB/s
      延迟累计次数 假设 comm->nChannels=4;A/B/C/D 共四个原任务,numPipeOps=ceil(4/4)=1,Ring、Tree 的 q 均为 1
      Tree pattern 假设 maxTreePattern=NCCL_TOPO_PATTERN_BALANCED_TREE,patternFactor=1
      默认 Simple 延迟 Ring:Lbase=8.4 µs、Lintra=5.7 µs;Tree:Lbase=8.4 µs、Lintra=4.0 µs
      默认 Tree 带宽上限 单节点 Hopper 的 perChMaxTreeBw=38.7 GB/s

      graphs[algo].nChannels 用于模型的带宽计算,comm->nChannels 用于调用前的任务数估计,二者作用不同;后续资源选择还会计算实际使用的 Channel 数。PCIe 类别的默认延迟常量按算法与协议区分,本例的 5.7 µs 和 4.0 µs 是模型参数,不能解释为 PCIe 5.0 链路的实测单跳延迟。

      下图把物理连接与调优规模分开表示:四个 rank 使用相同的任务规模,A/B 只在调优描述中累加元素数量,原任务与缓冲区仍分别保留。

      1. 上半部分表示通信载体。 四块 GPU 等距连接到同一个 PCIe switch,后续 Ring、Tree 图中的逻辑邻接均经过这一物理拓扑。
      2. 下半部分表示消息规模。 A 与 B 按 1∶2 的比例组成 18 KiB 的调优规模;S 取每 rank 的输入大小,不再乘四个 rank。
    6. 代入 Ring / Simple。 四个 rank 的 ReduceScatter、AllGather 各三步,共六步。Simple 保留 Bbus=48 GB/s,按 P/K=4/6 折算后,B_Ring=32 GB/s。

      text 复制代码
      L_Ring = 8.4 + 6 × 5.7 = 42.6 µs
      B_Ring = (24 × 2) × 4 / 6 = 32 GB/s
      传输项 = 18432 / (1000 × 32) = 0.576 µs
      T_Ring = 42.6 × 1 + 0.576 = 43.176 µs

      下图用环形连接、等宽数据块与六个阶段解释 Ring 的计算。环序取 GPU 0→1→2→3→0;实际顺序由拓扑搜索确定。为解释流量比例,按两个模型 Channel 均分数据;该均分仅用于推导,不代表后续 kernel 的实际分段结果。

      1. 沿环分块传输。 每个模型 Channel 分担 18/2=9 KiB,再按四个 rank 分成四块,每块 2.25 KiB;每步中各 rank 同时向下一个 rank 发送一块。
      2. 累计两阶段的发送量。 每 rank 每 Channel 共发送 6×2.25=13.5 KiB,两个 Channel 合计 27 KiB;27648/(1000×48)=0.576 µs,与算法有效带宽公式一致。
      3. 合并延迟项与传输项。 六步的延迟加上基础延迟得到 42.6 µs,再加 0.576 µs。图中的六步用于解释模型的阶段数,不表示六次独立的 kernel 启动。
    7. 代入 Tree / Simple。 单节点四 GPU 的链深度为三层,向上归约和向下广播各经过三层,因此延迟为 8.4+2×3×4.0=32.4 µs。Simple 不增加协议带宽折扣,但仍保留 Tree 的 AllReduce 修正和消息大小修正。

      text 复制代码
      B0 = 24 × 2 = 48 GB/s
      B1 = min(48 × 0.92, 2 × 38.7) = 44.16 GB/s
      Bstored = 44.16 × 1 × 0.5 = 22.08 GB/s
      
      S >> 6 = 18432 / 64 = 288
      k = floor(log2(288)) = 8
      treeCorrectionFactor[Simple][8] = 0.7
      B_Tree = 22.08 × 0.7 = 15.456 GB/s
      
      L_Tree = 8.4 + 2 × 3 × 4.0 = 32.4 µs
      传输项 = 18432 / (1000 × 15.456) ≈ 1.193 µs
      T_Tree = 32.4 × 1 + 1.193 ≈ 33.593 µs

      下图左右两列是同一条节点内链的两个通信方向,示例取 GPU 0 为根、GPU 3 为叶子;下方用逐渐缩短的带宽条表示模型的各次折算。

      1. 先归约,再广播。 左列从叶子向根归约,右列从根向叶子广播;每列三条边,对应延迟公式中的 2×3。这两列表示同一个 Channel 的两个方向。
      2. 逐项折算有效带宽。 带宽从 48 降到 44.16 GB/s;本例未触发 77.4 GB/s 的上限。再经双向折算和 k=8 的消息大小修正,得到 15.456 GB/s。
      3. 计算最终耗时。 用修正后的带宽计算传输项,与 32.4 µs 延迟项相加。0.92、0.5 和 0.7 分别来自不同规则,计算时均须保留。
    8. 比较本例结果。 在上述输入和默认常量下,Tree / Simple 的估算耗时较短。它的有效带宽低于 Ring / Simple,但本例消息只有 18 KiB,较低的延迟项主导了结果。

      组合 延迟项(µs) 有效带宽(GB/s) 传输项(µs) 总耗时(µs)
      Ring / Simple 42.600 32.000 0.576 43.176
      Tree / Simple 32.400 15.456 1.193 33.593

      该比较只覆盖两个 Simple 组合。实际调优还会评估通过筛选的 LL、LL128 等组合,因此本例不能据此确定完整候选集合的最终选择;表中结果也属于 Cost Model 演算值,实际耗时需通过运行测量。

  3. 调整估算耗时。

    有效的算法与协议组合形成后,若已加载 tuner 插件,ncclTuningCompute 调用 comm->tuner->getCollInfo,由插件调整表中各组合的估算通信耗时,并设置 Channel 上限。未加载插件时,保留 Cost Model 给出的估算耗时,直接进入组合选择。

    cpp 复制代码
    ncclResult_t (*getCollInfo)(void* context, ncclFunc_t collType,
                               size_t nBytes, int numPipeOps,
                               float** collCostTable, int numAlgo,
                               int numProto, int regBuff, int* nChannels);
  4. 选择最优组合。

    ncclTuningSelectBestTuning 遍历候选链表,比较各组合的估算通信耗时,将耗时最短的算法与协议组合保存到 bestTuning,供后续资源计算使用。已加载插件时,比较插件调整后的耗时;否则比较 Cost Model 给出的耗时。

    通用选择器在 selectionTimeUs>0 时优先使用该比较值,否则使用 timeUs;本文传统 Ring/Tree 路径使用 timeUs。比较值相等时,保留链表遍历中先遇到的组合。

  5. 计算资源并清理。

    选中算法和协议后,ncclTuningGetChannels 从 Channel 数、该算法/协议的最大线程数和工作量阈值出发,优化资源数量。它对 Ring/Tree、NVLS 和 CollNet 使用不同规则;这里先沿本文常用的 Ring/Tree 分支说明。

    1. 缩减 Channel。 nc 表示本次任务拟使用的 Channel 数,初始取 communicator 可用的 Channel 数 comm->nChannels,作为 Ring/Tree 调优的起始上限;nt 表示每个 Block 的线程数,初始取当前算法与协议对应的最大线程数 maxThreads[algo][proto]。当 nBytes < nc × nt × threadThreshold 且 nc>=2 时,逐次减少 nc,使小消息使用较少的 Channel。
    2. 缩减工作线程。 使用调整后的 nc,重新检查 nBytes < nc × nt × threadThreshold。若条件成立且 nt % 128 == 0,则将 nt 减半,再用更新后的 nt 检查该条件;当工作量条件不成立,或 nt 无法被 128 整除时,结束线程缩减。NVLS、NVLS_TREE 和 CollNet Direct 跳过这一通用缩减循环。
    3. 补充协议线程。 Simple 的 Ring 增加一个同步 warp,Tree 增加四个 warp;随后保证至少三个 warp。当前 Tree 实现最终将 nt 设为 NCCL_MAX_NTHREADS,Tree 的最终线程数以这次覆盖为准。
    4. 应用插件上限。 若插件给出的 maxChannels 为正且小于 nc,把 nc 限制到该值;否则把 maxChannels 更新为 nc。结果同时写入 nChannels=nc、nWarps=nt/WARP_SIZE。

    threadThreshold 表示每线程工作量的调优阈值,按字节计。条件 nBytes < nc × nt × threadThreshold 表示:按当前 Channel 数和每 Block 线程数分摊后,每线程的估计工作量低于阈值,需要考虑缩减资源。

    Ring/Tree 的默认值如下。

    协议 默认常量 Tree 阈值(byte/线程) Ring 阈值(byte/线程)
    LL NCCL_LL_THREAD_THRESHOLD=8 8 8 × nRanks
    LL128 NCCL_LL128_THREAD_THRESHOLD=8 8 8
    Simple NCCL_SIMPLE_THREAD_THRESHOLD=64 64 64
4.2.3.4 扩大候选范围并重新调优

任务指定的算法范围内未选出算法时,ncclGetAlgoInfo 可扩大候选范围重新调优;是否允许采用范围之外的结果,由 forceAlgSelection 决定。

  1. 重算的触发条件。 本次调优采用任务指定的算法范围(effAlgMask!=0),且首次未选出算法时,扩大到完整传统 kernel 候选范围重新调优。未采用任务指定范围,或首次已选出算法时,不触发这一重算。
  2. 重算仍受环境与能力约束。 扩大范围只解除本次任务的算法范围限制,环境配置、拓扑、运行时能力和 Cost Model 的支持条件继续生效。
  3. 允许回退时采用新结果。 重算成功且 forceAlgSelection=0 时,采用自动选择的结果;forceAlgSelection!=0 时,因原指定范围无法满足要求,即使重算成功,也返回 ncclInvalidArgument。
  4. 重算失败时返回错误。 重算中发生的内存分配失败、插件错误等会向上传递,本次调用不进入调优结果写回。
4.2.3.5 返回调优结果

计算成功且通过候选限制检查后,ncclGetAlgoInfo 将 bestTuning 的字段写回临时聚合任务 agg。

  1. 写入算法与协议。 bestTuning.algo、bestTuning.proto 分别写入 agg.algorithm、agg.protocol。

  2. 写入资源参数。 bestTuning.nWarps 表示每个 CUDA Block 配置的 warp 数,写入 agg.nWarps;每个 warp 包含 32 个线程,因此每 Block 的线程数为 nWarps×32。bestTuning.maxChannels 表示调优给出的 Channel 数量上限,非零时更新 agg.nMaxChannels,为零时保留传入值。后续编排还会根据 minCTAs、maxCTAs 等约束调整该上限,再确定任务实际使用的 Channel 数及编号。

  3. 写入预计集合通信耗时。 调用方提供 simInfo 时,将 bestTuning.timeUs 写入 simInfo->estimatedTime,单位为微秒(μs)。该值表示所选算法与协议执行当前聚合任务的预计集合通信耗时,由 Cost Model 估算,并可由调优插件调整。

  4. 示例中的四个任务。 沿用第 4.2.3.3 节的单节点四块 Hopper GPU、PCIe 5.0 拓扑及参数:Ring、Tree 的 bwIntra=24 GB/s、graphs[algo].nChannels=2,comm->nChannels=4,Tree pattern 为 NCCL_TOPO_PATTERN_BALANCED_TREE。本例将协议限定为 Simple,其余未指定的软件配置采用默认值,忽略调优插件。拓扑带宽和 Channel 数沿用前文的示例假设,不属于软件默认常量。

    1. 确定可用组合。 本例没有 NVLink/NVSwitch 路径,NVLS、NVLS_TREE 缺少可用拓扑;CollNet 在默认配置和本例拓扑下不可用,PAT 不支持 AllReduce,symmetric kernel 与 CE 不在本次传统 kernel 候选范围内。因此,在 Simple 协议约束下比较 Ring / Simple 与 Tree / Simple。

    2. 比较三次调优的耗时。 A/B 共享一次调优,规模为 (1536+3072)×4=18 KiB;C 独立调优,规模为 1536×8=12 KiB;D 独立调优,规模为 4608×4=18 KiB。四个原任务使 numPipeOps=ceil(4/4)=1,三次调用均沿用这一值。

      调优调用 消息大小 Ring / Simple(µs) Tree / Simple(µs) 选中组合
      A/B 共享 18 KiB 43.176 33.593 Tree / Simple
      C 独立 12 KiB 42.984 33.096 Tree / Simple
      D 独立 18 KiB 43.176 33.593 Tree / Simple
    3. 计算 Channel 上限与 warp 数。 默认初始值为 nc=4、nt=NCCL_SIMPLE_MAX_NTHREADS=512,Simple 的 threadThreshold=64。两种消息大小均使 nc 从 4 依次缩减到 1;此时工作量阈值为 1×512×64=32 KiB,仍需进入线程缩减。

      调优调用 消息大小 线程缩减后的 nt Simple 补充线程后 Tree 最终覆盖值
      A/B、D 18 KiB 256 256+4×32=384 640
      C 12 KiB 128 128+4×32=256 640

      当前 Tree 实现最后统一设置 nt=NCCL_MAX_NTHREADS=640,所以三次调优均得到 bestTuning.nWarps=640/32=20、bestTuning.nChannels=1、bestTuning.maxChannels=1。

    4. 按原任务列出返回结果。 ncclGetAlgoInfo 先将结果写入各次调用的临时 agg,第 4.3 节再把算法、协议和资源字段写回原任务。下表中的耗时属于该任务参与的调优调用。

      原任务 所属调优调用 算法 / 协议 nWarps nMaxChannels 该次调优耗时(µs)
      A A/B 共享,18 KiB Tree / Simple 20 1 33.593
      B A/B 共享,18 KiB Tree / Simple 20 1 33.593
      C C 独立,12 KiB Tree / Simple 20 1 33.096
      D D 独立,18 KiB Tree / Simple 20 1 33.593

      nWarps=20 表示每 Block 配置 640 个线程。D 的 maxCTAs=4 是后续编排的上限约束,不要求使用 4 个 Channel;本例调优得到的上限 1 无需因该配置再次缩减。A/B 两行的 33.593 µs 是同一次聚合调优的估算值,不能分别当作 A、B 单独执行的耗时。timeUs 保存在 bestTuning 中,仅当提供 simInfo 时写入 estimatedTime。

4.3 结果写回

ncclPrepareTasks 将调优结果写回各原任务,再按选定算法的资源类别,将任务追加到 collBins[isCollnet][isNvls]。四个队列用于组织后续调度所需的任务类别。

  1. 共享调优结果,保留独立任务。 同一聚合区间内的任务获得相同的算法、协议、Channel 上限、每 Block 的 warp 数和设备函数标识,同时保留各自的元素数量、缓冲区和 CTA 配置。

  2. 按算法确定目标队列。 isCollnet、isNvls 分别作为第一、第二维索引,表示任务所属的 CollNet、NVLS 资源类别。AllReduce 的分类规则如下。

    选定算法及条件 isCollnet isNvls 目标队列
    Ring、Tree 0 0 collBins[0][0]
    NVLS(单节点)或 NVLS_TREE 0 1 collBins[0][1]
    CollNet Direct、CollNet Chain 1 0 collBins[1][0]
    多节点 NVLS 1 1 collBins[1][1]
  3. 按处理顺序追加原任务。 各原任务依次加入对应队列的尾部,因此同一队列保留任务的处理顺序。采用不同算法、数据类型或归约操作的任务,只要两个分类标志相同,就可以进入同一队列。选用 LL 协议时,任务的 trafficBytes 另乘以 4,供后续调度估算使用。

沿用第 4.2.3.5 节的结果,A、B、C、D 均选择 Tree / Simple,两个分类标志均为 0。按本例的任务处理顺序,collBins[0][0] 中的队列为 A → B → D → C ,其余三个队列为空。A/B 共享调优后仍保留两个任务节点,D 的 maxCTAs=4 继续作为后续编排约束。

4.4 输出任务队列

所有任务分类完成后,ncclPrepareTasks 通过 ncclIntruQueueTransfer,将 collBins[2][2] 中的任务依次追加到 planner->collTaskQueue。

  1. 按固定顺序拼接。 四个来源队列的顺序为 collBins[0][0] → collBins[0][1] → collBins[1][0] → collBins[1][1],空队列不增加任务。
  2. 保留队列内部顺序。 每个来源队列整体追加到目标队列尾部,队内任务的相对顺序保持不变;目标队列原有任务仍位于前面。
  3. 转移后清空分类队列。 四个 collBins 队列均变为空队列,原任务由 planner->collTaskQueue 持有,供后续准备与编排使用。

沿用第 4.3 节的示例,只有 collBins[0][0] 非空。目标队列初始为空时,最终 planner->collTaskQueue 的任务顺序为 A → B → D → C,仍保留四个独立任务。

5. 连接需求与 Work 准备

ncclPrepareTasks 遍历 planner->collTaskQueue,根据算法和注册结果汇总连接需求,并为 NVLS 类任务提前准备 Work。

  1. 更新 NVLS 注册信息。 ncclRegisterCollNvlsBuffers 在满足条件时完成 NVLS 缓冲区注册,更新任务的注册类型和 Channel 上限,并返回注册地址。NVLS 注册成功时,regNeedConnect=false,表示后续无需再为该任务提出 NVLS 缓冲区资源需求;普通 Ring/Tree 任务不在此处执行 NVLS 注册。

  2. 按算法登记连接需求。 仅在 runtimeConn=true 且当前算法的 initAlgoChannels 尚未置位时,才登记新需求。需要连接的算法写入 algoNeedConnect,并将 needConnect 置为真;initAlgoChannels 同时置位,避免同一算法重复登记。该标记表示已登记需求,实际连接由后续阶段建立。

    当前算法 需要登记的连接需求
    Ring、Tree、CollNet 等其他算法 登记当前算法。
    NVLS 仅在 regNeedConnect=true 时登记 NVLS。
    NVLS_TREE 登记 NVLS_TREE;若 regNeedConnect=true 且 NVLS 尚未置位,还需补充 NVLS 需求。
  3. 为 NVLS 类任务保留 Work。 NVLS、NVLS_TREE 任务将缓冲区、归约参数、warp 数和注册状态等执行信息保存到 Work:带有 NCCL_NVLS_REG_BUFFER 标记时使用 ncclDevWorkCollReg,并附带 NVLS 注册地址;其余情况使用 ncclDevWorkColl。这些 Work 按任务顺序进入 planner->tmpCollWorkQueue,供后续 ncclTasksRegAndEnqueue 复用。Ring/Tree 等任务的 Work 留到后续阶段构造。

沿用本文四个任务均选择 Tree / Simple 的示例:若启用了运行时连接且 Tree 尚未置位,只需登记一次 Tree 连接需求;这四个任务均不会在本段代码中向 tmpCollWorkQueue 追加 Work。

6. 总结

任务预处理将独立的集合通信请求整理为带有算法、协议和资源参数的任务队列,供后续建立通信连接和生成执行计划使用。

  1. 任务类别与聚合条件决定共享调优范围。 集合操作、设备归约操作和数据类型相同的任务归为一类;同类任务中,流量规模满足条件且未被隔离的连续任务可以共享一次调优。共享的是调优结果,各任务仍保留独立的元素数量、缓冲区和调用配置。
  2. 算法选择受配置、能力和估算耗时共同约束。 环境配置、任务要求、拓扑及运行时能力决定可用的算法与协议组合,Cost Model 估算其通信耗时,插件可调整估算值。选择器据此确定最优组合;任务指定范围无法满足时,是否允许回退由 forceAlgSelection 决定。
  3. 调优参数与实际资源分配分阶段确定。 nWarps 表示每 Block 的 warp 数,每个 warp 包含 32 个线程;调优给出的 Channel 上限还要结合逐任务 CTA 配置和可用资源,由后续编排确定实际数量与编号。
  4. 最终队列保留原任务并按资源类别组织。 调优结果写回各原任务后,按 isCollnet、isNvls 分类,再按固定顺序汇入 planner->collTaskQueue。分类与队列拼接改变任务的组织顺序,共享调优的任务仍是独立节点。
  5. 记录连接需求,提前准备 NVLS Work。 algoNeedConnect、needConnect 汇总待建立的算法连接需求;NVLS 类任务还会提前处理注册并保留可复用的 Work。这些主机侧结果供后续阶段使用,预处理完成不代表 GPU 集合通信已经执行完成。

在本文四 GPU PCIe 拓扑、限定 Simple 协议且忽略插件的示例中,A/B 共享调优,C、D 分别调优;三次调用均选择 Tree / Simple,返回每 Block 20 个 warp 和 Channel 上限 1。四个原任务最终按 A → B → D → C 排入 planner->collTaskQueue。

6.1 关键参数与作用

下表保留影响任务分类、共享调优和资源分配的主要参数。单次调用配置通过 ncclCollConfig_t 传入,communicator 配置使用 ncclConfig_t。

参数或输入 主要影响 设置方式
Group 边界与 comm 确定批次范围;不同 communicator 分别整理任务。 ncclGroupStart、ncclGroupEnd 及集合通信接口的 comm 参数。
集合通信操作、op、datatype 决定任务类别;集合通信操作、设备归约操作和数据类型相同,才可归为一类。 选择集合通信接口,传入 op、datatype。
count 决定流量估计和消息大小,影响排序、聚合范围及估算通信耗时。 集合通信接口的 count 参数。
minCTAs、maxCTAs、nvlsCTAs 约束后续 CTA/Channel 分配;单次调用显式设置会使任务独立调优。 单次调用或 communicator 配置;对应环境变量为 NCCL_MIN_CTAS、NCCL_MAX_CTAS、NCCL_NVLS_NCHANNELS。
algSelection、forceAlgSelection 前者限定候选实现,非空时使任务独立调优;指定范围无法满足时,后者为 0 允许回退,非零则报错。 单次调用配置。
CTAPolicy 影响执行路径和调优策略;解析后的策略不同于 communicator 默认策略时,使任务独立调优。 单次调用或 communicator 配置;环境变量 NCCL_CTA_POLICY。
NCCL_ALGO、NCCL_PROTO、NCCL_SYM_KERNEL 限定算法、协议或 symmetric kernel;对当前集合通信操作生效的环境强制选择优先于 algSelection。 程序启动前设置环境变量。
用户缓冲区及注册状态 影响可用通信实现、估算耗时和资源需求。 传入 sendbuff、recvbuff,通过 ncclCommRegister 等接口注册缓冲区。
线程数与工作量阈值 影响 Channel 数和线程数的缩减;最终 nWarps 还受算法与协议规则约束。 环境变量 NCCL_NTHREADS、NCCL_LL128_NTHREADS、NCCL_THREAD_THRESHOLDS。

此外,rank 数、节点数、硬件拓扑和算法能力共同决定候选组合是否可用及其估算耗时。trafficBytes、aggIsolate 等字段由 NCCL 根据任务输入生成。

6.2 算法总结

Ring、Tree 的选择取决于延迟项与数据传输项的相对大小:小消息重点比较延迟,大消息重点比较有效带宽。下表依据第 4.2.3.3 节的通信组织与 Cost Model,归纳传统 AllReduce 路径中两种算法的特点和选择倾向。这里的消息大小指本次调优的 input.nBytes;共享调优时,使用聚合后的规模。

影响因素或场景 Ring 的特点 Tree 的特点 选择倾向与判断依据
组网与通信组织 沿逻辑环执行 ReduceScatter 和 AllGather,各 rank 分块收发,通过流水线持续传输数据。 沿树向上归约、向下广播;模型分别估算同一节点内各 rank 沿链式连接通信的开销,以及不同节点之间沿树形连接通信的开销。 环上路径能提供较高有效带宽时,大消息倾向 Ring;树形路径能减少跨节点延迟时,小消息倾向 Tree。
小消息,延迟项占主导 AllReduce 共经过 2×(nRanks−1) 个通信步骤,步骤累计的延迟对小消息影响较大。 模型按归约、广播经过的路径深度累计延迟。节点间树的深度随节点数呈对数增长,相比 Ring 按节点数累计的跨节点步骤,更有利于节点较多时的小消息通信。 通常倾向 Tree。 小消息对步骤延迟更敏感,Tree 的延迟项较低时,更有利于缩短总耗时。
大消息,数据传输项占主导 将数据分块并采用流水线传输,有利于利用链路带宽;较高的有效带宽可缩短大消息的数据传输时间。 支持按数据块组织流水线传输;模型将归约、广播两个阶段的传输开销计入有效带宽。有效带宽低于 Ring 时,相同消息大小对应的数据传输时间更长。 倾向 Ring。 前提是 Ring 的有效带宽更高,且传输时间的节省超过额外的步骤延迟。
节点数增加,或跨节点延迟较高 模型中的跨节点延迟按 2×(nNodes−1) 步累计,节点增多会放大这一开销。 模型用 2×floor(log2(nNodes)) 乘以跨节点单步延迟,估算归约、广播的跨节点延迟;节点内链式传递的延迟另行累计。 延迟敏感时倾向 Tree。 节点增多时,Tree 的跨节点延迟增长较慢;大消息仍需结合有效带宽比较总耗时。
NVLink、PCIe 或跨节点网络的带宽与延迟不同 使用 Ring 拓扑图的带宽、Channel 数和对应链路类别的延迟参数,路径带宽下降会增加数据传输时间。 节点内采用 NVLink 或 PCIe 的单步延迟,节点间采用网络延迟并计入拓扑附加延迟。模型先将每 Channel 的拓扑带宽乘以 Channel 数,再按协议效率、树形结构和消息大小修正,得到有效带宽。 小消息优先考虑延迟项较低者,大消息优先考虑有效带宽较高者。 将当前链路的延迟与带宽代入模型,选择估算总耗时较低的组合。
同一 Group 中有多个可共享调优的小任务 模型按 numPipeOps 次累计延迟。 模型将每 Channel 的任务数估计值换算为批次数,再按批次累计延迟,次数为 ceil(numPipeOps / NCCL_MAX_DEV_WORK_BATCH_COLLS)。 当 numPipeOps>1 且延迟项占主导时,倾向 Tree。 其模型累计延迟的次数少于 Ring。
本文四 GPU PCIe 示例:18 KiB、限定 Simple、无插件 模型估算为 43.176 µs,有效带宽较高,但延迟项较大。 模型的延迟项为 32.4 µs,数据传输项约为 1.193 µs,合计 33.593 µs;耗时主要来自延迟项。 选择 Tree / Simple。 较低的延迟项使估算总耗时比 Ring / Simple 少约 9.583 µs。

"小消息"和"大消息"的分界随拓扑、rank 数、协议及 Channel 资源变化。实际调优会比较配置允许且能力满足的算法与协议组合,并接受 tuner 插件调整;表中的倾向用于理解选择原因,具体结果以当前输入下的调优输出为准。

7. 参考资料

源码位置 核对内容
src/nccl.h.in、src/collectives.cc Group 与 AllReduce 公开接口、单次调用配置及初始化约定。
src/group.cc 预处理入口、回调、asyncJobLaunch、job 结果交接和连接需求的消费。
src/enqueue/enqueue.cc collTaskAppend 的任务登记,ncclPrepareTasks 的输入整理、分类聚合、结果写回、队列拼接和需求记录;ncclGetAlgoInfo 的输入输出。
src/include/comm.h task、planner、sorter 的结构和排序器插入、取出操作。
src/include/enqueue.h、src/include/device.h 接口声明、数量换算及 ncclDevFuncId 的主机表索引。
src/include/utils.h、src/misc/utils.cc 队列、内存池与内存栈操作,以及 ncclThreadJoin。
src/include/alloc.h、src/include/checks.h 主机分配、错误返回、NEW_NOTHROW 和线程创建宏。
src/os/linux.cc、src/include/compiler/gcc.h Linux CPU 亲和性接口、原子状态读写和位操作内建函数。
src/include/bitops.h、src/include/strongstream.h 流量区间编码及已记录 Graph 状态的有效性检查。
src/include/tuning.h、src/tuning/tuning_int.h 调优输入、结果、候选链表和接口声明。
src/include/plugin/nccl_tuner.h、src/include/sym_kernels.h、src/include/ce_coll.h 位图使用的算法、协议、对称内核与 CE 方法编号。
src/tuning/tuning.cc、src/tuning/tuning_general.cc 候选遍历、插件调用、估算耗时比较、Channel/warp 参数及结果清理。
src/tuning/ring.cc、src/tuning/tree.cc、src/tuning/collnet.cc、src/tuning/nvls.cc 各算法的 Cost Model 输入、能力检查和耗时估算。
src/include/plugin/tuner/tuner_v6.h、src/plugin/tuner.cc tuner 回调定义、加载与生命周期。
src/init.cc、src/graph/connect.cc、src/graph/trees.cc ring index、Channel 拓扑和双树构造。
src/device/all_reduce.h、src/device/prims_simple.h Ring/Tree 执行组织、chunk 尾块与 Simple slice 推进。
src/config/collconfig.cc、src/include/config/collconfig.h 聚合隔离条件和资源配置解析优先级。
src/config/algorithm_registry.cc、src/config/algorithm_parser.cc、src/tuning/cost_model.cc 候选位图、名称解析及环境配置对候选集合的影响。
src/register/coll_reg.cc 提前注册接口的 regNeedConnect 输出及资源参数调整。
相关推荐
2601_967987373 天前
Django ORM vs SQLAlchemy:到底谁更香?从入门到上头的选择指南
性能调优·sqlalchemy·团队协作·django orm·开发体验
爱好曙光20 天前
Redis Cluster 槽迁移:MOVED 重定向与拓扑刷新实战
redis·cluster·性能调优·moved·槽迁移
爱好曙光21 天前
SQLite WAL 高并发写入:busy_timeout 与 checkpoint 调优实战
sqlite·性能调优·checkpoint·wal·并发写入
Web3&Basketball23 天前
TorchTPU 迁移实战:3 个 CUDA 卡点
pytorch·性能调优·cuda·tpu·模型迁移·torchtpu
行者-全栈开发1 个月前
InfluxDB TSM 存储引擎深度解析:写入路径与压缩机制
cache·性能调优·influxdb·wal·存储引擎·compaction·tsm
gwf2161 个月前
内核态RDMA驱动调试技巧:ftrace/tracepoint/crash分析 —— 深入驱动底层的排错艺术
性能调优·linux驱动·ftrace·rdma·tracepoint·内核调试·crash分析
gwf2161 个月前
常见驱动问题定位与解决:设备识别、性能不达预期、CQ错误码 —— 资深RDMA驱动调试排错指南
驱动开发·linux内核·性能调优·rdma·故障排查·mlx5·irdma
gwf2161 个月前
SSD I/O路径全栈优化与Linux内核存储栈性能调优实战
性能优化·linux内核·nvme·性能调优·固态硬盘·存储技术·io_uring
桦仔1 个月前
自动数据库索引压缩合并
性能调优·聚集索引