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 完成语义消失。
系列位置
- URMA Mental Model:对象与数据路径
- URMA Write and Read:最小读写程序
- URMA Completion and Concurrency:完成、顺序与并发
- SHMEM UDMA Programming:对称内存与 put 接口
- AIV UDMA Direct Drive:
st_dev、多 QP 与 Relay
源码截面
本文的 QP 接口、Relay 约束和 UDMA 提交路径核对到 cann/shmem@59f0313325ef3179306ce6fa3bfef8ba2ff4cbc5。底层 WQE 布局不属于跨版本稳定教程接口,实际工程必须与目标 CANN、ops 包和硬件配套。
Host 提交与 AIV 直驱¶
常规 URMA Host 路径是:
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。这个过程可以概括为:
以本文追踪的 PUT 为例,上层信息并没有消失,而是在不同阶段被收窄成硬件真正需要的字段:
| 上层输入 | 内部 lowering | WQE 中的结果 |
|---|---|---|
aclshmemx_udma_put_nbi() |
选择内部操作类型 | WRITE opcode |
dst |
GetPhyAddr() 后做对称地址换算 |
SQE 的 remote_addr |
src |
GetPhyAddr() |
SGE 的本地 va |
elem_size 与 T |
elem_size * sizeof(T) |
SGE 的 message_len |
pe 与 qp_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 的堆基址上:
一个抽象例子更直观。假设 PE 0 和 PE 1 的对称堆基址分别是 0x1000_0000 和 0x9000_0000,dst 指向 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;
}
逐步解释:
lookup_qp(pe, qp_idx):从初始化阶段下发的表中找到目标 PE 的指定 QP。- 检查 credit:确认 SQ 还有设备未占用的槽位。
- 计算 slot:环形队列用 head 对深度取模选择物理槽位。
- 构造 WQE:填 opcode、源地址、目标地址、长度、token 和完成控制等字段。
- 写入 SQ:WQE 可先在 UB 临时区组装,再搬到 GM/HBM 中的 SQ。
- 可见性屏障:确保设备先看到完整 WQE,再看到新的 Doorbell 值。
- 敲 Doorbell:通知设备 SQ head 已推进。
- 保存 head:为本 QP 下一次提交准备索引。
buf 参数之所以出现在显式 UDMA put 中,就是因为 AIV 需要一块 UB 空间暂存 WQE。它不是为了把全部 payload 先复制进 UB。
st_dev 到底是什么¶
st_dev 是 Ascend C/CCE Device 侧 store 指令,用于从 AIV 向特定 Device 地址写值。它有多种数据宽度的重载,其中一个原型是:
这个命名很容易让人误会。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 区间:
- 比较设备消费位置与新的
cur_head。 - 从本地 HBM 的 SQ 中取出新 WQE。
- 解析 opcode、本地源地址、远端目标地址、长度、端点与 token。
- 从本地
srcGM 读取 payload,写入目标 PE 的remote_dst。 - 按 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_dev 的 offset 只修饰 Doorbell store 的目标地址,不描述 payload 位移。
最重要的边界有三条:
st_dev不是urma_post_jetty_send_wr()的另一种写法。st_dev只发布 Doorbell,不会替调用者构造 WQE。- WQE 对设备可见必须发生在 Doorbell 更新之前,否则设备可能读到半成品。
FFTS 文档与 UDMA 语境
公开 CCE Intrinsic 文档把 st_dev 放在 Vec/Cube 的 FFTS 同步章节,并解释了同步场景的 mode 与 flag_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。
标准接口与这份头文件处在同一条数据路径的不同位置:
理解这个区别后,问题就从“为什么不用 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 流水同步事件
但库内部仍需完成一整套提交动作:
- 用
pe找到远端内存与 QP 上下文。 - 将对称
dst换算成目标 PE 上的实际地址。 - 检查 SQ 是否还有 credit,必要时轮询 CQ。
- 填写 SQE 和 SGE,包括 opcode、EID、TPN、TID、token、源地址、目标地址与长度。
- 把 WQE 写入 SQ,推进 head 并敲 Doorbell。
- 记录预期 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 线程完成四步工作:
- 并行分类:每个线程读取一个 WQE 描述,根据目标 rank 选择 port、jetty 和本地 SQ。
- 汇总数量:统计每条 SQ 本批需要多少槽位。
- 批量预留:每条 SQ 只执行一次
atomicAdd(headAddr, count),取得一段连续 head 区间。 - 并行写入:各线程计算自己的
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 的布局中:
每个 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 不足前批量回收。
优势成立的四个前提
- 目标 Fabric 确实提供协议依赖的 512 B DataBlock 原子可见性。
asc_threadfence()与 WQE 的 GM 写入满足“完整 WQE 先于 Doorbell”的可见顺序。- SQ/CQ credit、CQE 错误和源 buffer 生命周期由当前文件之外的机制或补充实现可靠管理。
- WQE token 必须与当前 QP/MR 匹配;该实现虽然读取了
rmtTokenValue,simt_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 这个整数本身具有魔法。
对比错误写法:
多 QP Case 先在 Host 初始化前配置 QP 数。当前公共接口允许每个 peer 配置 1 到 ACLSHMEM_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 成立的四个条件¶
- 一核一 QP:同一个 QP 不再被多个无锁生产者共享。
- 独立 UB 临时区:不同 AIV 不覆盖彼此正在组装的 WQE。
- 独立数据切片:不同 QP 不无序写同一个远端 payload 区域。
- 分别等待完成:每个 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:真正发起请求的当前 PE。
- relay PE:提供中继出口或指定 Fabric 路径的 PE。
- target PE:最终数据要写入或读取的 PE。
Relay 的主要用途是显式选择可达路径或利用多条链路。最终内存对象仍属于 target PE;它不是要求应用先把 payload 写进 relay PE 的对称缓冲区,再由 relay Kernel 做第二次 put。
使用时应满足:
self、relay_pe和目标pe的角色明确,符合接口限制。- relay 所属端口确实能到达目标。
- 路径选择不会破坏上层要求的顺序和完成协议。
- 多路径写入仍使用不重叠切片或明确同步。
从入门到实际算子¶
建议按以下顺序增加复杂度:
- 单 PE 内确认 Kernel、对称分配和 signal 地址正确。
- 两 PE、单 AIV、单 QP 跑通一次 put-with-signal。
- 改成 nbi,并加入 quiet,验证缓冲区生命周期。
- 两 AIV、两 QP,各写一半不重叠数据,各用独立 signal。
- 增加轮次序号,验证重复运行不会读到旧 signal。
- 最后才引入 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,也不是应用层二次拷贝。
参考资料¶
- CANN SHMEM 源码仓
- SHMEM Device RMA 公共头文件
- UDMA Device 公共头文件
- UDMA Device 实现
- UDMA 多 QP 官方示例
st_devCCE Intrinsic API- 进阶实现:AIV Direct Drive Programming
- 实际场景:Ascend DeepEP MoE Dispatch Optimization