URMA Mental Model
先记住一条主线
一次最普通的远端写,可以先理解成下面这条流水线:
1 | 应用填写 WR |
这里最重要的判断是:WQE 不是与 SQ 并列的资源,WQE 是 SQ 中的一条工作项。
可以用餐厅来类比:
- WR 是服务员写下的点菜单,仍是软件容易理解的格式。
- WQE 是后厨系统识别的标准工单。
- SQ 是等待后厨处理的一排工单槽位。
- Doorbell 是通知后厨“有新单”的铃。
- CQE 是后厨写回的完成小票。
- JFC/CQ 是存放完成小票的队列。
这个类比只帮助建立直觉。编程时仍要回到对象、队列和内存权限三个维度。
对象关系
内存对象
| 概念 | 直观理解 | 在一次 WRITE 中的作用 |
|---|---|---|
| 本地缓冲区 | 普通虚拟地址 | 保存准备发送的数据 |
| Segment | 已向通信设备登记的一段内存 | 告诉设备地址范围和访问权限 |
urma_seg_t |
可交换的 Segment 描述 | 通过带外通道发给对端 |
urma_target_seg_t |
导入后的目标 Segment 句柄 | 真正用于访问本地或远端 Segment |
| SGE | 一个地址、长度和 Segment 句柄 | 描述一段连续数据 |
| SG | 一组 SGE | 描述可能不连续的完整数据 |
普通指针只对当前进程有意义。设备若要直接访问一块内存,还需要知道这块内存是否已注册、允许什么操作。因此,地址和 Segment 句柄必须配套使用。
对于远端内存,还要分清两步:
- 对端把可交换的
urma_seg_t描述发过来。 - 本端调用导入接口,把描述变成可提交给设备的
urma_target_seg_t *。
不要把“收到远端描述”误认为“已经能访问远端内存”。
队列与端点
| 概念 | 全称 | 作用 |
|---|---|---|
| JFS | Jetty for Send | 提供发送队列 SQ |
| JFR | Jetty for Receive | 提供接收队列 RQ |
| Jetty | JFS 与 JFR 的组合端点 | 统一承载发送和接收能力 |
| JFC | Jetty for Completion | 承载完成队列 CQ |
| SQ | Send Queue | 保存待设备执行的 WQE |
| RQ | Receive Queue | 保存接收方预先准备的接收 WQE |
| CQ | Completion Queue | 保存设备写回的 CQE |
Jetty 是通信端点对象,SQ 是它内部的一条发送队列。 一个 Jetty 可以被应用拿来提交请求,但应用通常不是直接把任意字节写入 SQ,而是先构造 URMA WR,再交给 Provider 编码和提交。
请求与完成
| 概念 | 所在层次 | 说明 |
|---|---|---|
| WR | URMA 软件接口 | 应用描述 opcode、源、目标、标志和上下文 |
| WQE | 设备队列格式 | Provider 根据 WR 编码出的硬件工作项 |
| CQE | 设备完成格式 | 设备写回的完成项 |
| CR | URMA 软件接口 | 应用从 JFC 轮询得到的完成结果 |
通常会把一个请求编号放入 wr.user_ctx。设备完成后,应用从 cr.user_ctx 取回同一个值,由此判断完成项属于哪个请求。这个编号只负责关联,不等于队列序号,也不自动保证数据顺序。
WRITE 到底描述了什么
一条 WRITE WR 至少回答四个问题:
- 从哪里读:本地源 SGE。
- 向哪里写:远端目标 SGE。
- 通过哪个端点发出:目标 Jetty 句柄。
- 完成后如何识别:完成标志和
user_ctx。
用结构关系表示:
1 | urma_jfs_wr_t |
complete_enable = 1 表示这个请求需要产生完成通知。若没有请求完成项,却一直轮询 JFC,程序不会得到预期结果。
单边与双边语义
URMA 的常见数据操作可以先分成两组:
| 操作 | 谁描述数据地址 | 接收端是否需要预投递 |
|---|---|---|
| WRITE | 发起端同时描述本地源和远端目标 | 不需要 |
| READ | 发起端同时描述远端源和本地目标 | 不需要 |
| SEND/RECV | 发送端描述发送数据,接收端描述接收缓冲区 | 需要先投递 RECV |
因此,WRITE/READ 是单边操作:目标进程不必为每一条传输即时调用接收函数。不过,目标内存仍然必须提前注册、授权并把描述交换给发起端。
基础 API 地图
不必一次记住全部函数,先按生命周期分组:
发现设备并创建上下文
urma_init()urma_get_device_list()或urma_get_device_by_name()urma_get_eid_list()urma_query_device()urma_create_context()
创建完成资源
urma_create_jfce():可选的完成事件对象。urma_create_jfc():创建完成队列。urma_poll_jfc():轮询完成结果。
创建通信端点
- 分开创建:
urma_create_jfs()、urma_create_jfr()。 - 组合创建:
urma_create_jetty()。 - 导入对端:
urma_import_jfr()或urma_import_jetty()。
- 分开创建:
注册并交换内存
urma_register_seg():注册本地 Segment。- 通过 socket 等带外通道交换 Segment 描述和 token。
urma_import_seg():导入远端 Segment。
提交数据请求
- 简便接口:
urma_write()、urma_read()、urma_send()、urma_recv()。 - 通用接口:
urma_post_jfs_wr()、urma_post_jfr_wr()。 - Jetty 接口:
urma_post_jetty_send_wr()、urma_post_jetty_recv_wr()。
- 简便接口:
按相反顺序清理
- 先确保请求已完成。
- 再取消导入、注销 Segment、删除 Jetty/JFC。
- 最后释放 Context 和设备列表。
st_dev 在哪里
st_dev 不是 URMA 的 Host 侧公共 API。它是 Ascend C/CCE 的 Device 侧写指令,可由 AIV 核在直驱 UDMA 时向 Doorbell 地址写入新的队列 head。
两条路径不要混在一起:
1 | Host 常规路径:WR → URMA API → Provider 填 WQE → CPU 写 Doorbell |
st_dev 只负责最后的 Doorbell 写入,不会替应用搬运 payload,也不会自动构造 WQE。AIV 直驱会在本系列第五篇单独展开。
学完本篇应能回答
- WQE 是什么?——设备能执行的一条工作项。
- Jetty SQ 是什么?——Jetty 内保存发送 WQE 的环形队列。
- WR 和 WQE 有何区别?——前者是软件请求,后者是 Provider 编码后的设备格式。
- 为什么要注册 Segment?——给设备提供地址范围、权限和可识别句柄。
- 为什么还要 JFC?——请求异步执行,应用需要从完成队列确认结果。
st_dev是 URMA API 吗?——不是,它属于 AIV 直驱路径中的 Device 指令。