3. Team 创建 API:nvshmem_team_create_stridednvshmem_team_create_2d 详解

好,咱们接着聊 Team 的创建。上一节讲了最基础的 nvshmem_team_split_strided,说白了就是按固定步长切分。但实际项目中,你遇到的拓扑结构往往更复杂。比如,你要做 2D 网格通信,或者需要把 GPU 按某种非均匀的 stride 分组。

这时候,nvshmem_team_create_stridednvshmem_team_create_2d 就派上用场了。我个人习惯把前者叫做「自由步长版」,后者叫做「网格拓扑版」。它们俩的底层逻辑完全不同,但都是构建复杂通信模式的核心工具。

3.1 nvshmem_team_create_strided:自由步长分组

这个 API 的签名长这样:

int nvshmem_team_create_strided(
    nvshmem_team_t parent_team,
    int start,
    int stride,
    int size,
    nvshmem_team_config_t *config,
    nvshmem_team_t *new_team
);

参数不多,但每个都有讲究:

  • parent_team:父团队,通常是 NVSHMEM_TEAM_WORLD 或者你之前 split 出来的子团队。
  • start:起始 PE 的全局索引。注意,这个索引是相对于父团队的。
  • stride:步长。相邻两个 PE 在父团队中的索引差。
  • size:新团队包含的 PE 数量。
  • config:配置结构体,一般传 NULL 用默认值。
  • new_team:输出参数,新团队的句柄。

举个例子,假设你有 8 个 PE(0~7),你想挑出索引为 1、3、5、7 的四个 PE 组成一个新团队:

nvshmem_team_t strided_team;
int start = 1;
int stride = 2;
int size = 4;

nvshmem_team_create_strided(
    NVSHMEM_TEAM_WORLD,
    start,
    stride,
    size,
    NULL,
    &strided_team
);

嗯,这里要注意:新团队内部的 PE 排名(rank)是按照你选取的顺序来的。start=1 的 PE 在新团队里是 rank 0,start+stride=3 的是 rank 1,以此类推。

我曾经踩过的坑: 步长 stride 不能为 0,否则会返回错误。另外,start + (size-1)*stride 不能超过父团队的 PE 总数减 1。我刚开始用的时候,总以为 stride 可以随便填,结果越界了,程序直接崩掉。后来养成习惯,每次创建前先算一下最大索引。

3.2 nvshmem_team_create_2d:网格拓扑创建

这个 API 就更有意思了。它专门用来创建 2D 网格状的团队结构。你想想看,很多科学计算、图像处理、矩阵运算,天然就是 2D 网格的通信模式。

函数签名:

int nvshmem_team_create_2d(
    nvshmem_team_t parent_team,
    int xrange,
    int yrange,
    nvshmem_team_config_t *config,
    nvshmem_team_t *new_team_x,
    nvshmem_team_t *new_team_y
);

参数解析:

  • parent_team:父团队。
  • xrange:X 方向的维度大小。
  • yrange:Y 方向的维度大小。注意,xrange * yrange 必须等于父团队的 PE 总数。
  • config:配置,传 NULL 即可。
  • new_team_x:输出参数,X 方向的行团队。每行包含 yrange 个 PE。
  • new_team_y:输出参数,Y 方向的列团队。每列包含 xrange 个 PE。

它的核心逻辑是:把父团队的 PE 按行优先(row-major)排列成一个 xrange 行、yrange 列的网格。然后,每一行形成一个行团队(X 方向),每一列形成一个列团队(Y 方向)。

举个例子,12 个 PE(0~11),你想排成 3 行 4 列:

nvshmem_team_t row_team, col_team;
int xrange = 3;  // 行数
int yrange = 4;  // 列数

nvshmem_team_create_2d(
    NVSHMEM_TEAM_WORLD,
    xrange,
    yrange,
    NULL,
    &row_team,
    &col_team
);

创建之后,PE 0、1、2、3 属于同一个行团队(第 0 行),PE 0、4、8 属于同一个列团队(第 0 列)。

我个人习惯: 在做 2D 网格通信时,我通常用行团队做数据分发(比如广播一行数据),用列团队做规约(比如列求和)。这样代码结构清晰,而且通信模式跟算法逻辑完全对应。

3.3 两个 API 的对比与选择

特性 nvshmem_team_create_strided nvshmem_team_create_2d
分组方式 按固定步长选取 PE 按 2D 网格行列分组
输出团队数 1 个 2 个(行团队 + 列团队)
适用场景 非均匀采样、环形拓扑 矩阵运算、图像处理、物理模拟
灵活性 高,步长可任意指定 低,必须满足网格约束
内部 rank 映射 按选取顺序映射 行团队按行内顺序,列团队按列内顺序

说白了,如果你的通信模式是「每隔几个 PE 取一个」,用 strided。如果你的通信模式是「按行或按列做集体操作」,用 2d。我在项目中遇到过好几次,有人用 strided 硬生生模拟 2D 网格,结果代码又长又容易出错。其实直接用 nvshmem_team_create_2d 就完事了。

3.4 实战:结合 2D 团队做矩阵转置

咱们来看一个实际例子。假设你要做矩阵转置,数据分布在 2D 网格上。每个 PE 持有矩阵的一个子块。转置操作需要每个 PE 跟同行同列的 PE 交换数据。

// 假设 4 个 PE,排成 2x2 网格
nvshmem_team_t row_team, col_team;
nvshmem_team_create_2d(NVSHMEM_TEAM_WORLD, 2, 2, NULL, &row_team, &col_team);

// 获取行团队和列团队中的 rank
int row_rank = nvshmem_team_my_pe(row_team);
int col_rank = nvshmem_team_my_pe(col_team);

// 行团队内做 alltoall:交换行内数据
nvshmem_team_alltoall(sendbuf, recvbuf, sizeof(float) * block_size, row_team);

// 列团队内做 alltoall:交换列内数据
nvshmem_team_alltoall(sendbuf, recvbuf, sizeof(float) * block_size, col_team);

你看,两行代码就完成了转置的核心通信。如果没有 2D 团队,你得手动计算每个 PE 的通信对,代码量至少翻三倍。

小技巧: 创建 2D 团队后,别忘了用 nvshmem_team_my_pe 获取当前 PE 在行/列团队中的局部 rank。这个 rank 在后续的集体操作中非常有用,比如决定当前 PE 负责哪一块数据。

3.5 避坑指南

  • 团队数量限制: NVSHMEM 对同时存在的团队数量有限制(通常是 8 个左右)。不要创建了不销毁,否则会耗尽资源。我见过有人循环创建团队,结果跑到第 9 次就报错了。
  • 销毁顺序: 先销毁子团队,再销毁父团队。否则父团队销毁后,子团队句柄就变成野指针了。
  • 配置参数: nvshmem_team_config_t 里有个 split_priority 字段,可以用来控制团队分裂时的优先级。默认值就够了,除非你有特殊的负载均衡需求。
  • 性能考量: 创建团队本身是同步操作,所有 PE 必须同时调用。如果某个 PE 调用晚了,整个程序会卡住。所以,团队创建最好在初始化阶段完成,不要在热路径上频繁创建销毁。

好了,关于这两个 API 的用法就讲到这里。记住,strided 适合自由步长,2d 适合网格拓扑。选对了工具,代码写起来顺手,性能也更容易优化。

Team 创建 API 核心逻辑 父团队 (Parent Team) nvshmem_team_create_strided nvshmem_team_create_2d 参数:start, stride, size 输出:1 个新团队 内部 rank = 选取顺序 适用:非均匀采样、环形拓扑 参数:xrange, yrange 输出:行团队 + 列团队 内部 rank = 行列顺序 适用:矩阵运算、物理模拟 新团队句柄 (new_team) 用于后续集体操作(广播、规约等) 注意:创建团队是同步操作,所有 PE 必须同时调用
专注资料整理