URMA Completion and Concurrency

导言

“WQE 和 Jetty SQ 还不能同时写?”这句话实际上混合了四个问题:软件和硬件能否同时访问队列、一个 SQ 能否挂多条请求、多个 Host 线程能否共享 Jetty、多个 AIV 能否直接驱动同一 QP。答案并不相同。本文先讲清提交与完成,再逐一划定并发边界。

系列位置

  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

源码截面

Host UDMA Provider 的锁、WQE 发布屏障和 Doorbell 路径核对到 openeuler/umdk@a2e11613f0f8174c0170413952ac4a5364a99a70。本文描述的是该实现截面,不把 Provider 内部机制外推成所有 URMA 设备的唯一实现。

两个环形队列

SQ 和 CQ 都可先看成带索引的环形数组:

1
2
3
4
5
SQ:软件写 WQE                                  设备读 WQE
producer index / PI ───────────────→ consumer progress

CQ:设备写 CQE 软件读 CQE
device producer progress ──────────→ consumer index / CI

正常情况下,软件可以填写尚未发布的 SQ 槽位,设备同时消费更早发布的槽位。这就是队列带来的生产者—消费者并行。

关键约束是:

  • 软件不能覆盖设备尚未消费的 WQE。
  • 软件必须先把完整 WQE 写到设备可见位置,再更新 PI 和 Doorbell。
  • 应用不能把尚未完成的本地缓冲区提前复用。
  • 软件消费 CQE 后,要按实现要求推进 CQ 的消费状态。

所以,“SQ 与 WQE 能否同时写”并不是准确问法。SQ 是容器,软件写的是 SQ 中尚且空闲的 WQE 槽位。

提交、执行与完成

一次请求至少经历三个不同时间点:

  1. 提交成功:URMA 接口接受 WR,Provider 已将其放入发送路径。
  2. 设备执行:UDMA 设备取到 WQE,并执行数据传输。
  3. 完成可见:设备生成 CQE,应用从 JFC 得到成功 CR。
1
2
3
4
post 返回                设备传输                  poll 得到 CR
│ │ │
▼ ▼ ▼
已提交 ─────────────────→ 执行中 ─────────────────→ 已完成

post 成功不能代替 poll 成功。尤其是非阻塞操作,源缓冲区的复用时间、目标数据的消费时间,都必须遵守接口定义的完成语义。

user_ctx 找回请求

多个请求同时在途时,不能只凭“我先提交了谁”来猜当前完成项对应谁。更稳妥的做法是给每条 WR 分配唯一上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (uint64_t i = 0; i < request_count; ++i) {
wr[i].user_ctx = base_rid + i;
wr[i].flag.bs.complete_enable = 1;
}

// 提交后持续轮询
urma_cr_t cr[16];
int n = urma_poll_jfc(jfc, 16, cr);
for (int i = 0; i < n; ++i) {
if (cr[i].status == URMA_CR_SUCCESS) {
mark_request_done(cr[i].user_ctx);
}
}

user_ctx 是应用关联信息。它能帮助找到请求,但不提供互斥、顺序或重试能力。

一次提交多条 WR

WR 的 next 字段可用于串起链表:

1
2
3
4
5
6
wr0.next = &wr1;
wr1.next = &wr2;
wr2.next = NULL;

urma_jfs_wr_t *bad_wr = NULL;
int ret = urma_post_jetty_send_wr(jetty, &wr0, &bad_wr);

这能减少调用开销,但要处理部分失败:

  • bad_wr == NULL:没有报告失败节点。
  • bad_wr != NULL:它指向第一条未成功提交的 WR;前面的 WR 可能已经进入 SQ。

批量不等于原子

一串 WR 被一次函数调用提交,不代表整串操作成为一个不可分割的事务。失败恢复时要根据 bad_wr 和完成项判断哪些请求已经提交或完成。

四种“同时写”

场景 是否可行 原因与条件
软件填写新 WQE,设备消费旧 WQE 可以 环形队列本来就为生产者和消费者并行设计,但不能越过容量边界
一个 SQ 中存在多条未完成 WQE 可以 这就是队列深度的意义;仍要处理信用、完成和缓冲区生命周期
多个 Host 线程调用同一 Jetty 提交 取决于 Provider 配置 公共接口可被并发调用,但具体 Provider 需要内部锁,或要求调用者在 lock-free 模式自行串行化
多个 AIV 直接更新同一 QP 的 head 和 Doorbell 不可以直接这样做 共享 head、SQ 槽位和 Doorbell,若无专门同步会产生覆盖与丢更新

Host 多线程

在常规 Host 路径中,应用调用 URMA 接口,Provider 负责构造 WQE。当前 UDMA Provider 的常规配置会在提交临界区使用自旋锁,大致保护以下步骤:

1
2
3
4
5
6
7
加锁
→ 检查 SQ 是否有空闲槽位
→ 填写一条或多条 WQE
→ 推进 SQ PI
→ 内存屏障
→ CPU 写 Doorbell
解锁

因此,多个线程可以发起调用,但同一 SQ 的实际发布步骤仍需要串行化。如果启用 Provider 的 lock-free 配置,锁被移除不表示冲突消失,而是互斥责任转移给调用者。

最容易维护的策略是:

  • 单个 QP 由一个提交线程拥有;或者
  • 多线程共享时在更高层明确加锁;或者
  • 为独立流量分配不同 QP。

AIV 多核直驱

AIV 直驱绕开 Host Provider,由 Device 代码直接构造 WQE、推进 head 并写 Doorbell。多个 AIV 若同时使用同一 QP,可能出现:

1
2
3
4
AIV 0 读取 head = 8
AIV 1 读取 head = 8
AIV 0 写 SQ[8],准备发布 head = 9
AIV 1 也写 SQ[8],准备发布 head = 9

最终至少有一条 WQE 被覆盖,而且 head 只前进了一次。这不是 payload 地址是否相同的问题,而是队列元数据本身发生竞争

SHMEM UDMA 的常见并行方式是给不同 AIV 分配不同 QP,使每个 AIV 独占自己的 SQ/CQ/head/Doorbell。第五篇会给出对应代码。

多 QP 也不自动解决数据竞争

多 QP 解决的是队列生产者冲突,并不自动解决业务数据冲突:

1
2
3
QP 0 ── WRITE 100 bytes ──┐
├──→ 同一个远端地址
QP 1 ── WRITE 100 bytes ──┘

两条路径若同时写同一远端地址,最终值取决于实际执行与可见顺序。正确做法通常是让不同 QP 操作不重叠的数据片段,或者在协议层建立明确同步。

同理,不同 QP 之间不能仅凭提交先后推断远端可见顺序。需要跨 QP 依赖时,应使用完成、signal、barrier 或上层协议表达。

顺序与完成不是一回事

可以先用两个问题区分:

  • 顺序问题:操作 B 会不会在操作 A 之后被观察?
  • 完成问题:操作 A 是否已经执行到可以安全复用资源的程度?

Fence 类语义主要约束操作顺序,Quiet 或完成项主要回答在途操作是否完成。某个实现可能让 Fence 比规范要求更强,但可移植代码不应依赖这种偶然增强。

对于“先写数据,再通知对端”的协议,最直接的表达不是两次无关联的普通写,而是使用带顺序保证的 signal 操作,或者显式建立写完成与通知之间的依赖。本系列第四篇会用 putmem_signal 展开。

队列满时怎么办

SQ 深度有限。生产速度持续高于设备消费速度时,提交会遇到信用不足或队列满。处理策略包括:

  1. 轮询并回收完成项。
  2. 限制同时在途请求数。
  3. 使用批量投递降低单次调用开销,但仍遵守队列容量。
  4. 在真正存在独立流量时增加 QP,而不是无条件扩大并发。

完成队列也必须及时消费。只投递、不轮询,最终可能让完成资源耗尽。

缓冲区复用规则

对于非阻塞 WRITE,可用一条保守规则避免大部分错误:

1
2
3
4
写入本地源缓冲区
→ 提交请求
→ 等到对应成功完成
→ 才修改或复用源缓冲区

READ 则是:

1
2
3
提交 READ
→ 等到对应成功完成
→ 才读取本地目标缓冲区

如果为了性能提前复用,必须确认所用接口给出的更精确完成定义,而不能靠时间延迟猜测。

本篇结论

  • WQE 可以排在 SQ 中并保持多条在途,这是正常工作方式。
  • 软件与设备可以分别操作不同的队列位置,前提是索引、容量和可见性协议正确。
  • 同一 Host Jetty 的多线程提交需要 Provider 或调用者串行化关键更新
  • 多个 AIV 不应无同步地直驱同一 QP,常见方案是一核一 QP。
  • 多 QP 只隔离队列状态,不隔离远端数据地址
  • 提交成功、顺序成立、操作完成是三个不同判断

参考资料

Author

Shaojie Tan

Posted on

2026-09-02

Updated on

2026-09-02

Licensed under