跳转至

AIV UDMA Direct Drive

导言

刚理解 WQE 时,我看到一份 MoE dispatch 头文件没有调用 aclshmemx_udma_put_nbi(),也没有在发送热路径调用 aclshmemx_udma_quiet()。第一反应是:它是不是用 SIMT 加速了 WQE 下发,又用 Data-as-Flag 代替了完成通知?本文先沿标准接口追踪 Tensor 如何被降成地址、长度与 WQE,再把它与头文件中的批量直驱实现逐项比较。最后得到的边界是:SIMT 加速的是提交,Data-as-Flag 加速的是接收端就绪判断;二者都没有让 UDMA、SQ/CQ 生命周期或源 buffer 完成语义消失。

系列位置

  1. URMA Mental Model:对象与数据路径
  2. URMA Write and Read:最小读写程序
  3. URMA Completion and Concurrency:完成、顺序与并发
  4. SHMEM UDMA Programming:对称内存与 put 接口
  5. AIV UDMA Direct Drive:st_dev、多 QP 与 Relay

源码截面

本文的 QP 接口、Relay 约束和 UDMA 提交路径核对到 cann/shmem@59f0313325ef3179306ce6fa3bfef8ba2ff4cbc5。底层 WQE 布局不属于跨版本稳定教程接口,实际工程必须与目标 CANN、ops 包和硬件配套。

Host 提交与 AIV 直驱

常规 URMA Host 路径是:

Host 应用构造 WR
    → 调用 urma_post_jetty_send_wr
    → UDMA Provider 编码并写入 WQE
    → CPU 写 Doorbell
    → UDMA 执行

AIV 直驱路径是:

Host 预先创建 Endpoint/Channel、注册内存并下发资源表
AIV 根据 pe 和 qp_idx 查资源
    → 换算远端地址
    → 在 UB 中组装 WQE
    → 将 WQE 写入 HBM SQ
    → 更新 head
    → st_dev 写 Doorbell
    → UDMA 执行

二者的主要区别是谁位于每次通信操作的提交热路径。AIV 直驱并不意味着 Host 完全消失:资源创建、注册、建链和状态下发仍然需要 Host 控制面。

QP 是什么

QP,Queue Pair,是一条带独立队列状态的通信通道。 在本文关注的 UDMA 发送路径中,最重要的是它拥有或关联一组独立状态:

  • SQ 与 WQE 槽位。
  • SQ head/PI 和设备消费进度。
  • CQ 与完成统计。
  • Doorbell 地址。
  • 目标端点、内存访问与通道信息。

PE 和 QP 解决不同问题:

参数 回答的问题 示例
pe 数据发给谁 发给 PE 3
qp_idx 通过到该 PE 的哪条队列通道提交 使用 PE 3 的 QP 1

一个 PE 可以对应多个 QP。它们都通向同一个逻辑参与者,但拥有各自的队列生产者状态。

Tensor 到 WQE 的边界

对上层算子来说,最合适的停靠点确实是 Tensor 或连续地址区间,而不是 WQE 位域。但这里的 Tensor 不能直接类比成一个带完整 shape、stride 和 layout 的框架 Tensor。AscendC::GlobalTensor<T> 在这条路径上更像带类型的 GM 地址视图

它在公开重载中会很快被降成设备可见地址、远端换算结果和字节长度:

auto dst_phy_addr = (__gm__ T*)dst.GetPhyAddr();
auto src_phy_addr = (__gm__ T*)src.GetPhyAddr();
auto remote_dst = aclshmem_ptr(dst_phy_addr, pe);
uint64_t message_len = elem_size * sizeof(T);

之后才进入内部 WQE builder。这个过程可以概括为:

GlobalTensor<T>
  → 物理地址 + 元素数
  → 本地地址 + 远端地址 + 字节数 + 操作语义
  → SQE / SGE 位域

以本文追踪的 PUT 为例,上层信息并没有消失,而是在不同阶段被收窄成硬件真正需要的字段:

上层输入 内部 lowering WQE 中的结果
aclshmemx_udma_put_nbi() 选择内部操作类型 WRITE opcode
dst GetPhyAddr() 后做对称地址换算 SQE 的 remote_addr
src GetPhyAddr() SGE 的本地 va
elem_sizeT elem_size * sizeof(T) SGE 的 message_len
peqp_idx 选择 PE/QP 资源表 EID、TPN、TID、token 及队列上下文
CONFIG 编译期语义到位域 CQE、fence 和 ordering domain 等控制位

普通 put/get 只关心连续字节,T 主要用于计算字节数;只有 reduce 或 AMO 一类具有数值语义的操作,类型才可能继续影响操作属性。这是一层有意的隔离:上层选择“做什么”,SHMEM 内部决定“每一位怎么填”。

公开的是语义,不是 WQE ABI

aclshmemx_udma_op_config_t 暴露的是 CQE、fence 和 ordering domain 等语义开关。具体的移位、掩码、WQE 大小和 token 布局仍属于内部实现,不是面向普通算子开发者的稳定接口。

对称地址如何进入 WQE

dst 在接口上是对称地址表达,而不是直接交给 UDMA 的远端裸地址。SHMEM 先计算它在当前 PE 对称堆中的偏移,再把这个偏移加到目标 PE 的堆基址上:

offset     = dst - local_heap_base
remote_dst = remote_heap_base[pe] + offset

一个抽象例子更直观。假设 PE 0 和 PE 1 的对称堆基址分别是 0x1000_00000x9000_0000dst 指向 PE 0 堆中的 0x2000 偏移,那么 WQE 里的实际远端目标就是 0x9000_2000

对称内存是 SHMEM 的软件寻址约定,不是 UDMA 的 WQE 位域。 UDMA 最终看到的仍然是具体远端地址、长度、端点和访问 token。

非连续 Tensor 怎么办

这层抽象也有清楚的边界。如果一个 [M, N] Tensor 在 GM 中连续,上层只需传入起始地址和 M * N 个元素,一条 WQE 就可以覆盖。

x[:, 0] 这类切片在物理上可能不连续。普通 aclshmemx_udma_put_nbi() 不会从 GlobalTensor 自动推导 shape 和 stride;它仍会从起始地址连续读取 elem_size * sizeof(T) 字节。此时需要明确选择:

  • Pack 后传输:先把非连续切片收拢到连续缓冲区,再提交一条 PUT。
  • 拆分连续子块:按行或其他连续区间提交多条 WQE,必要时使用批量提交。
  • 使用 stride 语义接口:SHMEM 通用 RMA 中有 iput/iget 类接口,但它不等于显式 UDMA PUT 自动完成 Tensor lowering,具体引擎路径仍需按当前实现确认。

AIV 如何提交一条 UDMA WQE

下面是按源码职责归一化后的伪代码,不是可独立编译的公共接口:

void post_udma_write(int pe, int qp_idx,
                     __gm__ uint8_t *remote_dst,
                     __gm__ uint8_t *src,
                     uint32_t bytes,
                     __ubuf__ uint8_t *wqe_buf)
{
    UDMA_QP *qp = lookup_qp(pe, qp_idx);

    wait_until_sq_has_credit(qp);

    uint32_t slot = qp->head % qp->sq_depth;
    build_write_wqe(wqe_buf, remote_dst, src, bytes, qp);

    copy_wqe_from_ub_to_sq(qp->sq_base, slot, wqe_buf);

    uint32_t new_head = qp->head + 1;
    make_wqe_visible_before_doorbell();
    st_dev(new_head, qp->doorbell_addr, 0);
    qp->head = new_head;
}

逐步解释:

  1. lookup_qp(pe, qp_idx):从初始化阶段下发的表中找到目标 PE 的指定 QP。
  2. 检查 credit:确认 SQ 还有设备未占用的槽位。
  3. 计算 slot:环形队列用 head 对深度取模选择物理槽位。
  4. 构造 WQE:填 opcode、源地址、目标地址、长度、token 和完成控制等字段。
  5. 写入 SQ:WQE 可先在 UB 临时区组装,再搬到 GM/HBM 中的 SQ。
  6. 可见性屏障:确保设备先看到完整 WQE,再看到新的 Doorbell 值。
  7. 敲 Doorbell:通知设备 SQ head 已推进。
  8. 保存 head:为本 QP 下一次提交准备索引。

buf 参数之所以出现在显式 UDMA put 中,就是因为 AIV 需要一块 UB 空间暂存 WQE。它不是为了把全部 payload 先复制进 UB。

st_dev 到底是什么

st_dev 是 Ascend C/CCE Device 侧 store 指令,用于从 AIV 向特定 Device 地址写值。它有多种数据宽度的重载,其中一个原型是:

void st_dev(int32_t src, __gm__ int32_t *dst, int16_t offset);

这个命名很容易让人误会。src寄存器中的标量值,不是源内存地址;dst 是设备侧目标地址;offset 是相对 dst 的字节偏移。在直驱 UDMA 的固定源码截面中,真实调用是:

__gm__ uint32_t *door_bell_addr =
    (__gm__ uint32_t *)qp_ctx_entry->db_addr;
st_dev(cur_head, door_bell_addr, 0);

三个参数在这条路径上分别表示:

  • cur_head:SQ 更新后的 Producer Index,即要写入 Doorbell 的 32 位值。
  • door_bell_addr:当前 PE 上、当前 QP 对应的 UDMA Doorbell MMIO 地址,它不是远端 payload 地址。
  • 0:相对 Doorbell 基址没有额外字节偏移。

因此,st_dev(cur_head, door_bell_addr, 0) 的直接效果是cur_head 值写到 Doorbell 地址,而不是从 cur_head 地址向 Doorbell 搬运数据。

Doorbell 之后发生了什么

Doorbell 更新不是 OS 系统调用,也不需要回到 Host 再提交一次。AIV 通过标量流水向 UDMA 的设备映射地址写入新 head 后,UDMA 才开始处理新暴露的 SQ 区间:

  1. 比较设备消费位置与新的 cur_head
  2. 从本地 HBM 的 SQ 中取出新 WQE。
  3. 解析 opcode、本地源地址、远端目标地址、长度、端点与 token。
  4. 从本地 src GM 读取 payload,写入目标 PE 的 remote_dst
  5. 按 WQE 配置更新 CQ/CQE,由后续 aclshmemx_udma_quiet(pe) 收敛完成语义。

这样再看“从什么地址到什么地址”,其实是三条不同路径:

阶段 从哪里到哪里 搬运内容
发布 WQE UB scratch → 本地 HBM SQ 通信描述符
更新 Doorbell 标量寄存器 → UDMA Doorbell MMIO 新的 SQ head
执行 PUT 本地 src GM → 远端 remote_dst HBM 真正的 Tensor payload

只有最后一条才是业务数据传输。st_devoffset 只修饰 Doorbell store 的目标地址,不描述 payload 位移。

最重要的边界有三条:

  1. st_dev 不是 urma_post_jetty_send_wr() 的另一种写法。
  2. st_dev 只发布 Doorbell,不会替调用者构造 WQE
  3. WQE 对设备可见必须发生在 Doorbell 更新之前,否则设备可能读到半成品。

FFTS 文档与 UDMA 语境

公开 CCE Intrinsic 文档把 st_dev 放在 Vec/Cube 的 FFTS 同步章节,并解释了同步场景的 modeflag_id。而本文固定的 SHMEM 源码截面明确传入 cur_head 和 UDMA db_addr。理解这一行时应以当前 SHMEM/HCOMM 下发的 Doorbell 资源语义为准,不应把 cur_head 重新解码成普通 FFTS flag_id

不要手写未知布局

WQE、Doorbell、token 和队列状态是强版本相关的数据结构。除非正在开发或适配底层 Provider,不要脱离目标版本的 cann/shmem/HCOMM 实现自行猜字段布局。

为什么头文件不调用 put_nbi

看到 deepep_moe_dis_dispatch_split_rank.h 直接写 SQ 时,最容易先得出一个过大的结论:这份实现绕过了 UDMA。其实不是。它绕过的是 aclshmemx_udma_put_nbi() 的逐次提交路径,最终搬运 payload 的仍然是 UDMA。

标准接口与这份头文件处在同一条数据路径的不同位置:

标准接口:业务参数 → put_nbi → 库构造并提交 WQE → UDMA 搬运
头文件:  业务参数 → SIMT 批量构造并提交 WQE → UDMA 搬运

理解这个区别后,问题就从“为什么不用 UDMA”变成了更具体的一问:为什么不让库一次提交一条 WQE,而要自己成批生产?

标准接口已经做了什么

从调用者看,一次显式 UDMA Put 只有六个主要参数:

aclshmemx_udma_put_nbi<T>(
    dst,        // 目标 PE 上的对称地址表达
    src,        // 当前 PE 的本地 GM 源地址
    buf,        // PIPE_MTE3 路径暂存 WQE 的 UB 空间
    elem_size,  // T 元素数量
    pe,         // 目标 PE
    sync_id);   // MTE3 流水同步事件

但库内部仍需完成一整套提交动作:

  1. pe 找到远端内存与 QP 上下文。
  2. 将对称 dst 换算成目标 PE 上的实际地址。
  3. 检查 SQ 是否还有 credit,必要时轮询 CQ。
  4. 填写 SQE 和 SGE,包括 opcode、EID、TPN、TID、token、源地址、目标地址与长度。
  5. 把 WQE 写入 SQ,推进 head 并敲 Doorbell。
  6. 记录预期 CQE,供后续 aclshmemx_udma_quiet() 收敛完成状态。

这条路径适合作为默认实现:调用边界清楚,队列检查、token 和完成记账由库负责。它的问题不是“慢”,而是当 MoE dispatch 需要同时发出大量小而不规则的 token 时,逐条执行相同的控制步骤不一定是最合适的粒度。

SIMT 怎样批量生产 WQE

头文件把 AIV 分成了不同角色:一部分核打包 token,一部分核专门负责 URMA/UDMA 写入。发送核不是立刻逐 token 调 API,而是先生成一批 WQE 描述,再调用 simt_nw_mj()

simt_nw_mj<PORT_NUM, JETTY_NUM>(
    tokenSendWqeDesc,
    wqeCount,
    sqs,
    startRankId,
    sendRankNum,
    remoteWriterId,
    ...);

simt_nw_mj_vf() 使用最多 128 个 SIMT 线程完成四步工作:

  1. 并行分类:每个线程读取一个 WQE 描述,根据目标 rank 选择 port、jetty 和本地 SQ。
  2. 汇总数量:统计每条 SQ 本批需要多少槽位。
  3. 批量预留:每条 SQ 只执行一次 atomicAdd(headAddr, count),取得一段连续 head 区间。
  4. 并行写入:各线程计算自己的 absoluteHead,调用 simt_write_wqe_v4() 写入对应 WQE 槽位。

核心动作可以缩成下面几行:

const uint32_t baseHead =
    atomicAdd((__gm__ uint32_t *)(mySq->headAddr), count);

const uint32_t absoluteHead =
    sqBaseHead[localSqIdx] + localOffset;

simt_write_wqe_v4(
    &sqs[localSqIdx], absoluteHead,
    localAddr, remoteAddr, messageLen, opcode);

所有线程写完后,asc_threadfence() 保证 WQE 写入先于发布动作。标量侧随后遍历本批实际使用的 SQ,每条 SQ 只敲一次 Doorbell:

if (sqWqeCountLT.GetValue(localSqIdx) > 0U) {
    st_dev(
        headLT.GetValue(localSqIdx),
        reinterpret_cast<__gm__ uint32_t *>(
            dbLT.GetValue(localSqIdx)),
        0);
}

这不是简单地把 for 循环换成 128 个线程,而是同时改变了构造、排队和发布的粒度

提交环节 逐次 put_nbi() 头文件的 SIMT 直驱
QP/MR 元数据 每次调用沿库路径获取 每个 writer 先缓存到 UB,再由一批 WQE 复用
WQE 构造 调用路径逐条构造 最多 128 个 SIMT 线程并行构造
SQ head 每条操作推进一次 每条 SQ 每批只原子预留一次连续区间
Doorbell immediate 路径通常随操作发布 每条活跃 SQ 每批只发布一次
目标分配 API 调用前由调用者确定 PE/QP 同一 SIMT 批次中跨 rank、port、jetty 分流
完成保护 库维护 CQE 计数与 credit 需要直驱实现自己补齐 CQ/SQ 生命周期

当前 cann/shmem 已经提供 defer/submit 聚合接口,但一个 batch 要求使用相同的操作类型、PE 参数、QP、UB base 和 sync_id。这份头文件需要在一次 SIMT 调度中处理多个目标 rank 和多条 SQ,因此自定义批量器仍然表达了标准 batch 之外的调度策略。

真正的性能优势

这套设计的优势不是“手写 WQE 天生更快”,而是让大量 token 共享元数据查询、head 预留和 Doorbell 发布,并让 WQE 编码在 SIMT 线程间并行。收益能否覆盖额外的分类、原子操作和 UB 占用,仍需在目标硬件、消息大小与 rank 数下实测。

Data-as-Flag 怎样接上发送路径

SIMT 解释了“WQE 怎么更快地下发”,但还没有解释为什么发送热路径不等待 aclshmemx_udma_quiet()。答案出现在 token buffer 的布局中:

constexpr uint64_t SPLIT_BLOCK_SIZE = 512UL;
constexpr uint32_t SPLIT_BLOCK_DATA_SIZE = 480U;

每个 512 B DataBlock 被组织成:

0                                             480          512 B
┌─────────────────────────────────────────────┬──────────────┐
│                 480 B payload               │  32 B flag   │
└─────────────────────────────────────────────┴──────────────┘

发送侧先把通信 buffer 填成 1.0F,再只覆盖各块的 480 B payload,于是尾部 32 B 自然保留为到达标记。UDMA WQE 搬运整个 token;接收侧则从每块偏移 480 B 的位置读取 flag,只有一个 token 的全部块都等于 1.0F,才将它交给后续计算。消费完成后再把 flag 清零,并通过 dataState_ 双 buffer 降低跨轮复用旧状态的风险。

这正是 Data-as-Flag Communication 中的 Token-Flag Fusion(TFF):payload 与完成证明共享同一个 512 B DataBlock。在目标 Fabric 保证 512 B DataBlock 原子可见的前提下,接收方看见 flag,就可以推断同一块 payload 已完整可见,不必再等待一条独立 signal。

于是数据路径可以细化为:

Stage AIV 打包 {payload, flag}
    → PublishStageDone
    → Writer AIV 批量写 WQE
    → 每条 SQ 敲一次 Doorbell
    → UDMA 写远端 512 B DataBlock
    → 接收 AIV 轮询内嵌 flag
    → 按已到达 token 继续计算

WaitAllStageDone() 只保证 Doorbell 发布前,本地 send buffer 已经打包完毕。它发生在网络传输之前,不是 quiet() 的替代实现。

为什么仍不能忘记 quiet 和 CQ

Data-as-Flag 只回答接收端最关心的问题:“这一块数据现在能不能读?”它没有同时回答发送端和队列的另外几个问题:

完成层次 这份头文件中的机制 是否等价于 quiet()
本地 payload 已准备好 PublishStageDone() / WaitAllStageDone() 否,尚未开始远端传输
WQE 已向硬件发布 st_dev(head, dbAddr, 0) 否,只代表设备可以取单
远端 DataBlock 可消费 接收端检查 32 B 内嵌 flag 否,只建立远端就绪证明
源 buffer 可复用、SQ/CQ 可回收 CQE polling / quiet / 等价自定义机制 是这层需要解决的问题

头文件中确实定义了 URMAPollCQ(),但当前 URMASendToken() 调用链没有使用它。同时,simt_write_wqe_v4() 写入的 WQE flag 为 0x20,即启用了 CQE;SQ head 批量预留前也没有看到与标准接口相同的 credit 检查。

这形成了一个尚未由当前文件单独证明闭环的边界:

  • 可能由外围统一回收:另一个 kernel 或 Host 生命周期负责消费 CQE。
  • 可能依赖有界调用量:单轮 WQE 数不会耗尽 SQ/CQ,资源在更高层重建或复位。
  • 也可能尚未补齐:重复运行后存在 CQ 堆积、SQ credit 耗尽或源 buffer 过早复用风险。

因为直驱路径没有经过标准 put_nbi() 的完成记账,直接在末尾补一行 aclshmemx_udma_quiet(pe) 也未必能够覆盖这些手工提交。更完整的设计应让自定义提交与自定义 CQ polling 使用同一套 head、tail 和 CQE 计数,并在阶段末或 credit 不足前批量回收。

优势成立的四个前提

  1. 目标 Fabric 确实提供协议依赖的 512 B DataBlock 原子可见性。
  2. asc_threadfence() 与 WQE 的 GM 写入满足“完整 WQE 先于 Doorbell”的可见顺序。
  3. SQ/CQ credit、CQE 错误和源 buffer 生命周期由当前文件之外的机制或补充实现可靠管理。
  4. WQE token 必须与当前 QP/MR 匹配;该实现虽然读取了 rmtTokenValuesimt_write_wqe_v4() 中却将相应字段写成 0,需要结合目标版本确认它是否确实允许零 token。

所以,这个 Case 最准确的结论不是“SIMT 和 Data-as-Flag 让 quiet 消失了”,而是:

SIMT 将逐 WQE 提交改造成批量、多队列的并行生产;Data-as-Flag 将接收端完成判断下沉到每个 DataBlock。二者压缩了热路径,但发送完成与队列回收仍需另一条可靠的状态链。

指定 QP 为什么可以并行

假设一个 PE 配置两个 QP,并由两个 AIV 各自独占:

AIV block 0 → QP 0 → SQ 0 / head 0 / CQ 0 / Doorbell 0
AIV block 1 → QP 1 → SQ 1 / head 1 / CQ 1 / Doorbell 1

两个 AIV 不再同时读改写同一个 head,也不会争抢同一个 SQ slot,所以可以独立生产 WQE。这里的并行来自队列元数据和槽位被隔离,不是 qp_idx 这个整数本身具有魔法。

对比错误写法:

AIV block 0 ─┐
              ├→ 同一个 QP → 同一个 head 和 SQ slot → 数据竞争
AIV block 1 ─┘

多 QP Case 先在 Host 初始化前配置 QP 数。当前公共接口允许每个 peer 配置 1ACLSHMEM_MAX_QP_NUM 个 QP,其中当前最大值为 32,而且所有 PE 必须传相同值:

// Host 侧,必须在 aclshmemx_init_attr() 之前调用。
int status = aclshmemx_set_qp_num(
    ACLSHMEM_DATA_OP_UDMA,
    qp_count);
if (status != ACLSHMEM_SUCCESS) {
    return status;
}

Device Case 可以按以下方式组织:

int block = GetBlockIdx();
int qp_idx = block;

if (block >= qp_count) {
    return;
}

uint64_t offset = block * slice_bytes;

aclshmemx_udma_qp_put_nbi<uint8_t>(
    remote_dst + offset,
    local_src + offset,
    wqe_ub_for_this_block,
    slice_bytes,
    peer_pe,
    qp_idx,
    sync_id_for_this_block);

aclshmemx_udma_qp_quiet(peer_pe, qp_idx);

Host 启动的 block 数应等于 qp_count,这样 GetBlockIdx() 可以直接作为 qp_idx,每个 QP 恰好只有一个 AIV 提交者。wqe_ub_for_this_block 也必须是当前 block 独占、容量不少于 ACLSHMEM_UDMA_MTE_STAGING_UB_SIZE 的 UB 临时区。

多 QP 成立的四个条件

  1. 一核一 QP:同一个 QP 不再被多个无锁生产者共享。
  2. 独立 UB 临时区:不同 AIV 不覆盖彼此正在组装的 WQE。
  3. 独立数据切片:不同 QP 不无序写同一个远端 payload 区域。
  4. 分别等待完成:每个 QP 的在途请求按其完成接口收敛,再进入跨核或跨 PE 同步。

如果只满足第一条而多个 QP 仍写同一远端地址,队列冲突消失了,业务数据竞争仍然存在。

多 QP 的完成与 signal

每个 QP 有独立进度,通常也应为不同生产者准备独立 signal:

QP 0 写 slice 0 → signal[0] = round
QP 1 写 slice 1 → signal[1] = round

接收端分别等待 signal[0] 和 signal[1]
两者都满足后才消费完整 buffer

共用一个 signal 会带来两个问题:

  • 多个 writer 对同一值执行 SET 时,接收端无法知道哪些切片已到。
  • 即使使用 ADD,也要处理原子性、轮次复用和计数溢出协议。

初学阶段优先使用“一 QP 一切片一 signal”,先确保正确,再考虑压缩同步状态。

Relay 不是 Replay

接口或注释中的 Relay 表示“中继路径”,不是“重放请求”的 Replay。

一条 Relay UDMA 请求涉及三个角色:

self PE ──经 relay PE 的端口/链路路径──→ target PE
  • self PE:真正发起请求的当前 PE。
  • relay PE:提供中继出口或指定 Fabric 路径的 PE。
  • target PE:最终数据要写入或读取的 PE。

Relay 的主要用途是显式选择可达路径或利用多条链路。最终内存对象仍属于 target PE;它不是要求应用先把 payload 写进 relay PE 的对称缓冲区,再由 relay Kernel 做第二次 put。

使用时应满足:

  • selfrelay_pe 和目标 pe 的角色明确,符合接口限制。
  • relay 所属端口确实能到达目标。
  • 路径选择不会破坏上层要求的顺序和完成协议。
  • 多路径写入仍使用不重叠切片或明确同步。

从入门到实际算子

建议按以下顺序增加复杂度:

  1. 单 PE 内确认 Kernel、对称分配和 signal 地址正确。
  2. 两 PE、单 AIV、单 QP 跑通一次 put-with-signal。
  3. 改成 nbi,并加入 quiet,验证缓冲区生命周期。
  4. 两 AIV、两 QP,各写一半不重叠数据,各用独立 signal。
  5. 增加轮次序号,验证重复运行不会读到旧 signal。
  6. 最后才引入 Relay、更多 PE、轮转调度和性能流水。

每一步至少检查:源数据、目标数据、signal、QP 完成状态和错误码。不要只看 Kernel 没有报错。

本篇结论

  • AIV 直驱改变的是提交者,UDMA 仍是执行数据搬运的引擎。
  • GlobalTensor<T> 重载会降成地址与字节数,不会自动传递 shape、stride 或 layout。
  • SHMEM 把对称偏移换算成具体远端地址,UDMA 硬件不需要理解“对称内存”抽象。
  • st_dev 负责 Doorbell store,不负责 payload 搬运或 WQE 构造。
  • 相对逐次 put_nbi(),SIMT 直驱可以并行编码 WQE,并按 SQ 合并 head 预留与 Doorbell 发布。
  • Data-as-Flag 让接收端按 512 B DataBlock 或 token 判断就绪,但不能替代源 buffer 完成与 SQ/CQ 回收。
  • QP 是独立队列通道;PE 指目标参与者,QP 指到该参与者的提交车道。
  • 指定不同 QP 能并行,是因为 SQ、head、CQ 和 Doorbell 状态被隔离。
  • 多 QP 不自动提供跨 QP 顺序,也不消除远端地址冲突。
  • Relay 是中继选路,不是 Replay,也不是应用层二次拷贝。

参考资料

评论