Memory Semantics vs RDMA

导言

MPI、RDMA 和内存语义都能搬运数据,但它们让应用表达的是不同对象:MPI Point-to-Point 表达 rank、tag 和消息,RDMA Verbs 表达 QP、WQE 和完成记录,内存语义则尽量让应用只表达 地址、长度、读写方向和数据依赖

后一种方式的收益不是“凭空消灭通信”,也不是“任何场景都比 RDMA 带宽高”。它真正改变的是通信契约:把外围队列事务下沉为更接近计算流水的地址访问与 tile 可见性,使 Scale-Up 域内的短操作、细粒度生产消费和异步流水更容易表达。

结论

内存语义相对 MPI 与 RDMA 的五个收益,可以先压缩成以下判断:

  1. 相对普通 MPI Send/Recv 更简单:应用不再逐次匹配 rank + tag + send buffer + recv buffer,而是发布一个可寻址窗口,再对地址执行 Put/Get/Load/Store。需要注意,MPI RMA 本身也已经提供单边 Put/Get,因此不能把“所有 MPI”都等同于双边消息。
  2. 减少应用可见的通信缓冲:生产者和消费者可直接操作注册窗口或对称内存,不必先把张量打包进独立 Send/Recv staging buffer。注册池、tile buffer、协议状态和必要的内部队列仍然存在,准确说法是减少 bounce 与显式 staging,不是“系统里没有 buffer”。
  3. 完成路径更接近计算流水:传统 Verbs 以 WQE 提交、CQE 或 event 完成为主;MTE、Load/Store Engine 等专用搬运流水可以用 stream event、数据依赖或 tile-ready 信号驱动计算。RDMA 本身同样有硬件卸载,差别在硬件放置位置与完成语义,不是“有硬件对无硬件”。
  4. Scale-Up 域内可缩短外围链路:外围 RNIC 路径通常包含 WQE、doorbell、QP 状态、PCIe/RNIC DMA 与 CQE;片上或片间控制器可让计算侧更直接地调度存储侧。GPUDirect RDMA 已能绕过 host bounce,因此这里比较的是进一步减少 RNIC/QP/CQE/PCIe handoff,不是声称所有 RDMA 都经过 CPU 拷贝。
  5. 更自然地支持细粒度边传边算:消费者可以等待 tile i ready,而非把一个完整 tensor 对应的元操作完成作为唯一边界。RDMA 也能提交小 WQE、使用 unsignaled completion 并并发多个请求;内存语义的优势是把这种流水写成地址范围和计算依赖,而不是由应用维护更多队列事务。

一句话

MPI 把数据当消息,RDMA 把搬运当队列事务,内存语义把远端数据当带显式顺序与生命周期的地址对象。

MPI:匹配消息

MPI Point-to-Point 的核心对象是消息 envelope。发送方提供 source buffer、count、datatype、destination 和 tag,接收方提供 receive buffer、source 和 tag;运行时先完成匹配,再推进数据与请求状态。MPI-4.1 Point-to-Point

MPI 消息匹配语义五视图
MPI Point-to-Point 的对象、流程、时序和生命周期。图中 buffer 是应用可见对象;不同 MPI 实现仍可对内部路径做零拷贝或硬件优化。

一个典型双边过程可以规范化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def exchange_with_mpi(rank, peer, tensor, tensor_shape, dtype, communicator):
element_count = product(tensor_shape)
send_buffer = allocate_contiguous(element_count, dtype)
recv_buffer = allocate_contiguous(element_count, dtype)
pack_tensor(tensor, send_buffer)

receive_request = mpi_irecv(
buffer=recv_buffer,
count=element_count,
dtype=dtype,
source=peer,
tag=17,
communicator=communicator,
)
send_request = mpi_isend(
buffer=send_buffer,
count=element_count,
dtype=dtype,
destination=peer,
tag=17,
communicator=communicator,
)

mpi_wait(receive_request)
received_tensor = unpack_tensor(recv_buffer, tensor_shape, dtype)
consume(received_tensor)

mpi_wait(send_request)
release(send_buffer)
release(recv_buffer)

这里不意味着 MPI 必然执行两次真实内存拷贝。若 tensor 已经连续、实现支持设备内存或协议能直接使用用户 buffer,pack/unpack 可以被省略或融合。问题在于应用仍需围绕双方 buffer、消息匹配和 request 生命周期组织控制流

其延迟可以用一个非精确但有助于定位成本的分解表示:

$$
T_{\text{MPI}}
\approx
T_{\text{pack}}
+T_{\text{match}}
+T_{\text{transport}}
+T_{\text{unpack}}
+T_{\text{wait}}.
$$

MPI RMA 已经提供 Window、Put、Get、Accumulate 和同步 epoch,使 origin 可以单边访问 target window。MPI-4.1 RMA Communication Calls MPI-4.1 RMA Synchronization 因此,“内存语义比 MPI 简单”应限定为:相对普通 Point-to-Point 消息匹配更直接,同时把单边寻址进一步做成跨 CPU、NPU 和存储介质的统一数据面。

RDMA:提交远端事务

RDMA 将远端 CPU 从逐次收包和拷贝中移出数据面。应用注册 Memory Region,交换 remote_addr + rkey,构造包含 SGE、opcode 和远端地址的 Work Request,再提交到 Queue Pair。完成结果由 Completion Queue 返回。ibv_post_send(3) ibv_poll_cq(3)

RDMA 队列事务语义五视图
传统 RDMA Verbs 路径。RDMA 已由 RNIC 硬件执行 DMA、可靠传输和部分原子操作;图中比较的是队列事务、外围控制器与完成记录,而不是软件拷贝对硬件拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def write_with_rdma(context, queue_pair, completion_queue, local_tensor, remote_ref):
local_region = register_memory(
context=context,
address=local_tensor.data_ptr,
length=local_tensor.nbytes,
access_flags=["LOCAL_WRITE"],
)

scatter_gather_entry = {
"addr": local_region.addr,
"length": local_tensor.nbytes,
"lkey": local_region.lkey,
}
work_request = {
"wr_id": next_work_request_id(),
"opcode": "RDMA_WRITE",
"send_flags": ["SIGNALED"],
"sge": [scatter_gather_entry],
"remote_addr": remote_ref.addr,
"rkey": remote_ref.rkey,
}

post_send(queue_pair, work_request)

completed = False
while not completed:
completions = poll_completion_queue(completion_queue, max_entries=16)
for completion in completions:
if completion.status != "SUCCESS":
raise_transport_error(completion)
if completion.wr_id == work_request["wr_id"]:
completed = True

deregister_memory(local_region)

RDMA 的事务化接口带来成熟的可靠传输、隔离、路由和生态,也引入一组应用或运行时要维护的对象:MR、QP、SGE、WQE、CQE、doorbell、重试与错误状态。一个简化延迟模型是:

$$
T_{\text{RDMA}}
\approx
T_{\text{reg,amortized}}
+T_{\text{post}}
+T_{\text{queue}}
+T_{\text{fabric}}
+T_{\text{remote-memory}}
+T_{\text{CQE}}.
$$

不要把 RDMA 描述成 host bounce

NVIDIA GPUDirect RDMA 允许 PCIe peer device 直接访问 GPU memory,避免先落到 host memory;但它仍需要内存映射与 pinning,并受 PCIe 拓扑和设备能力约束。NVIDIA GPUDirect RDMA

因此,内存语义在 Scale-Up 域内强调的是:计算单元能否通过片上/片间控制器直接发起数据访问,并减少外围 RNIC、QP、CQE 和 PCIe 域切换,而不是“RDMA 一定经过 CPU”。

内存语义:用地址表达位置

内存语义把控制面与数据面拆开:

  • 控制面发布 GVA/对称地址 + size + dtype + generation + 权限
  • 数据面按地址范围执行 Load、Store、Get、Put 或 xcopy;
  • runtime 根据源、目的和拓扑选择 MTE、URMA、RDMA 或 host copy;
  • 完成通过 event、signal、fence 或 completion handle 发布;
  • 地址引用在所有消费者和 in-flight tile 完成前保持有效。

UnifiedBus 的公开资料将内存编程模型作为 SuperPoD 互联的重要组成;MemFabric 则公开介绍了以 Global Virtual Address 和 xcopy 类接口统一多种搬运引擎的设计。UnifiedBus Huawei UnifiedBus launch MemFabric

内存地址语义五视图
内存语义把“数据在哪里、走哪条路径”从应用逐次编排中下沉到 runtime。统一地址不自动提供 cache coherence、全局原子性、立即可见性或故障恢复。

一个支持 tile pipeline 的上层接口可以是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def consume_remote_tensor(memory_runtime, stream, remote_ref, tile_bytes):
window = memory_runtime.lookup_window(
global_address=remote_ref.gva,
generation=remote_ref.generation,
required_permission="READ",
)
slots = [
memory_runtime.allocate_local_tile(tile_bytes),
memory_runtime.allocate_local_tile(tile_bytes),
]
ready_events = [stream.create_event(), stream.create_event()]
done_events = [stream.create_event(), stream.create_event()]

tile_count = ceil_div(remote_ref.nbytes, tile_bytes)
for tile_index in range(tile_count):
slot_index = tile_index % 2
if tile_index >= 2:
stream.wait(done_events[slot_index])

offset = tile_index * tile_bytes
valid_bytes = min(tile_bytes, remote_ref.nbytes - offset)
memory_runtime.get_async(
destination=slots[slot_index],
source_window=window,
source_offset=offset,
nbytes=valid_bytes,
stream=stream,
completion=ready_events[slot_index],
)

stream.wait(ready_events[slot_index])
compute_tile(
tile=slots[slot_index],
valid_bytes=valid_bytes,
stream=stream,
)
stream.record(done_events[slot_index])

for slot_index in range(2):
stream.wait(done_events[slot_index])
memory_runtime.release_local_tile(slots[slot_index])

memory_runtime.release_window_lease(window)

这个接口并没有消灭底层 RDMA。跨更远的节点时,runtime 仍可能把 get_async 编译成 WQE;在 HCCS 连接的设备间,它也可能选择 MTE。收益是上层控制流围绕地址、tile 与依赖不变,物理 route 由 runtime 适配

简化后的成本模型是:

$$
T_{\text{MEM}}
\approx
T_{\text{map,amortized}}
+T_{\text{issue}}
+T_{\text{fabric}}
+T_{\text{remote-memory}}
+T_{\text{order}}.
$$

相对 RDMA,主要减少机会位于 post/queue/completion 一侧;映射、权限、顺序和故障处理并不会消失。

MTE:搬运进入流水

华为 Ascend 文档将 MTE 定义为 AI Core 的 Memory Transfer Engine / Load-Store Unit。常见流水中,MTE1 负责 L1 与 L0 之间的数据移动,MTE2 负责 Global Memory 到 Local Memory,MTE3 负责 Local Memory 到 Global Memory;不同搬运流水可与 Vector/Cube 计算流水并行,并通过 event 保证依赖。Ascend MTE glossary Ascend C pipeline

Ascend MTE 搬运与 tile 流水五视图
MTE 是计算侧搬运执行资源,不是跨集群寻址协议。远端地址、权限、路由和完成仍由 SHMEM、MemFabric 或其他 runtime 提供。

Ascend SHMEM 的 stream API 在固定版本代码中会根据拓扑选择路径:HCCS 可用时走 MTE,其他支持场景走 RDMA;其 device API 还提供 remote GM 与 local UB 之间的非阻塞 MTE Put/Get,包括非连续搬运参数。这说明 MTE 可以参与统一内存语义的数据面,但 MTE 本身不是这个语义层Ascend SHMEM repository

双缓冲流水可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def mte_double_buffer_pipeline(source, destination, tile_count, tile_shape):
local_tiles = [
allocate_unified_buffer(tile_shape),
allocate_unified_buffer(tile_shape),
]
ready_events = [create_event(), create_event()]
done_events = [create_event(), create_event()]

for tile_index in range(tile_count + 2):
load_index = tile_index
compute_index = tile_index - 1
store_index = tile_index - 2

if load_index < tile_count:
load_slot = load_index % 2
if load_index >= 2:
wait_event(done_events[load_slot])
mte2_copy_async(
destination=local_tiles[load_slot],
source=source.tile(load_index),
)
record_event(ready_events[load_slot])

if 0 <= compute_index < tile_count:
compute_slot = compute_index % 2
wait_event(ready_events[compute_slot])
vector_or_cube_compute(local_tiles[compute_slot])
record_event(done_events[compute_slot])

if 0 <= store_index < tile_count:
store_slot = store_index % 2
wait_event(done_events[store_slot])
mte3_copy_async(
destination=destination.tile(store_index),
source=local_tiles[store_slot],
)

wait_all_mte_operations()
release_unified_buffer(local_tiles[0])
release_unified_buffer(local_tiles[1])

跨 device 有物理边界

Ascend C DataCopy 的跨 device 支持受产品代际与 HCCS 物理连接约束。Ascend C DataCopy 跨超节点或更远 Scale-Out 拓扑仍可能需要 URMA、RDMA 或其他 xDMA。可行架构是统一语义、分层 route,不是让一条 MTE 指令覆盖任意距离。

五个收益的准确说法

用户直觉 准确的工程表述 仍然存在的成本
相对 MPI 更简单 相对普通 Send/Recv,不再逐次匹配 rank/tag 和双方 buffer;对地址窗口执行单边访问 MPI RMA 已有相似抽象;窗口同步、权限和 epoch 仍要管理
无通信 buffer 无应用可见的独立 Send/Recv staging 或 host bounce;可直接访问注册/对称窗口 注册池、UB tile、协议状态、网络 credit 和必要队列仍存在
不同于 CQE,有专用硬件 搬运引擎更靠近计算与内存流水,用 event/依赖驱动 tile 可见性 RDMA 也有 RNIC 硬件卸载;错误与最终完成仍需状态对象
绕过 NIC-GM-cache 长链路 在适用 Scale-Up 拓扑中,可减少外围 RNIC、QP/CQ、PCIe 和多次域切换 GPUDirect 已绕 host bounce;远距离仍需 NIC/transport,cache coherence 也非自动获得
不等整个元操作结束 以 cache line、burst 或 tile 发布 ready,消费者可对已到达部分开算 RDMA 也可小包、并发和 unsignaled;过细粒度会被固定开销、乱序和事件流量反噬

这五点背后的共同变化是:

1
2
3
MPI:  identity(rank, tag) + two-sided buffers
RDMA: identity(QP, WR) + remote memory key + completion
MEM: identity(address range, generation) + dependency/visibility

细粒度为何能边传边算

假设一个 tensor 被切成 $N$ 个 tile。整体搬完再计算的时间近似为:

$$
T_{\text{bulk}}
\approx
\sum_{i=0}^{N-1} T_{\text{move},i}
+
\sum_{i=0}^{N-1} T_{\text{compute},i}.
$$

双缓冲流水稳定后,则近似为:

$$
T_{\text{pipeline}}
\approx
T_{\text{fill}}
+
\sum_{i=0}^{N-1}
\max\left(T_{\text{move},i},T_{\text{compute},i}\right)
+
T_{\text{drain}}.
$$

真正关键的不是传输 API 名字,而是完成边界是否与消费者所需的最小数据单元对齐

  1. 生产者发布 tile i 的地址范围和 generation;
  2. 搬运引擎只拉取或写入这个 tile;
  3. 写入完成后先执行必要的 memory ordering;
  4. 再发布 ready[i]
  5. 消费者只等待 ready[i],开始计算;
  6. tile i+1 同时在搬运;
  7. 最后一个消费者完成后,slot 才能被复用。

若仍以整个 tensor 的 CQE 或全局 barrier 作为可见性边界,即使链路带宽很高,也会形成头部阻塞。反过来,tile 太小会让寻址、event、packet header、credit 和乱序管理占比升高,因此必须通过实测找到 crossover,而不是默认越细越好。

路径证据

OpenURMA 给出了一个可以检查机制的公开实现:它把 UB Controller 放在 on-chip bus,与外围 PCIe RNIC 路径对照,并将 Load/Store 短路径与 URMA Work Request 路径分开。OpenURMA arXiv

OpenURMA Figure 1:RoCE 与 UB 控制器位置及状态比较
OpenURMA Figure 1。左侧是 PCIe 后的 RNIC、WQE/CQE rings 与 per-QP 状态,右侧是 on-chip bus 上的 UB Controller、Load/Store 或 membus doorbell。来源:arXiv:2605.28717v1。

其 Figure 19 在作者控制的 matched model 中设置单向链路延迟 100 ns、并发 1、payload 64 B(barrier 为 8 B)。pointer chase 的 UB Load/Store 路径为 500 ns,RoCE DMA-WQE 路径为 2186 ns;bulk write 和 FAA barrier 也展示了相同实现内不同路径的固定成本差异。

OpenURMA Figure 19:小粒度操作延迟
OpenURMA Figure 19。这里是 64 B/8 B、100 ns 单向链路、并发 1 的特定建模与测量条件,不能外推为任意 payload、任意拓扑或商用 Ascend 芯片的通用倍数。

证据边界

OpenURMA 是 2026 年 5 月提交的 arXiv v1,结果来自 clean-room RTL、SystemC 与 gem5 实现及匹配的 OpenRoCE 对照。它不是商用 Ascend 950 实测,也尚不能替代端到端 RL 业务 benchmark。这里使用它证明路径机制和固定成本可以不同,不把 500 ns / 2186 ns 当成产品承诺。

适用边界

更有优势的场景

  • Scale-Up 域内短操作:控制器与计算、内存处在更近的硬件域,外围队列与 PCIe handoff 占比较高。
  • 多消费者复用:控制面传播引用,消费者按需读取字段或 tile,避免重复序列化整对象。
  • 异步生产消费:数据字段在不同时刻 ready,消费者只等待自己的地址范围。
  • 多模态与 RL 数据面:图像、视频、token、mask、logprob、value 和 reward 的大小与到达时间不同,适合 field/tile 级可见性。
  • 通信计算融合:搬运与 Vector/Cube 流水有显式事件,可以稳定执行 move i+1 // compute i // store i-1

RDMA 更稳妥的场景

  • Scale-Out 与跨厂商互通:网络距离长、故障面大,需要成熟的可靠传输、拥塞控制、隔离与运维工具。
  • 大块顺序搬运:payload 足够大时,队列与完成固定成本被摊薄,带宽而非提交延迟占主导。
  • 现有系统已围绕 Verbs 优化:使用 registered pool、BlueFlame、inline data、unsignaled completion、batch posting 和 GPUDirect 后,迁移收益可能有限。
  • 需要成熟错误语义:地址语义若没有 generation、lease、权限、完成与恢复协议,容易把显式队列错误变成更隐蔽的 stale pointer 与提前复用。

对 RL 基础设施而言,合理架构不是删除 RDMA,而是:

1
2
3
4
5
6
TensorRef / FieldRef API

topology-aware memory runtime

MTE or LD/ST | URMA | RDMA | host copy
Scale-Up fast path Scale-Out fallback

这一结论也直接约束 VeRL TransferQueue 的后端设计:TransferQueue 继续管理 key、field、ready、consumed 和 retry;内存 runtime 管理地址、搬运、完成、lease 与 generation;MTE/RDMA 只是可替换的执行引擎。

总结

内存语义的核心价值不是发明另一种“更快的 memcpy”,而是把通信的第一性对象从消息或队列事务改成可寻址数据与依赖关系

  • 对 MPI Point-to-Point,它减少双方匹配和显式 staging 的编排;
  • 对 RDMA Verbs,它把 QP/WQE/CQE 隐藏在 topology-aware runtime 之下;
  • 对 MTE,它让搬运成为计算侧流水的一部分;
  • 对异步 RL 和多模态,它允许 field/tile 先到先算、按需复用;
  • 对整个系统,它同时要求更严格的地址代际、可见性、租约、权限和故障恢复。

最终是否更快,取决于 payload、拓扑、固定提交成本、内存位置、复用次数和计算重叠。最值得验证的不是单次峰值带宽,而是同一业务 workload 下的 controller RSS、bounce bytes、首 tile 延迟、pipeline overlap、尾延迟与故障恢复。

参考资料

Author

Shaojie Tan

Posted on

2026-07-27

Updated on

2026-07-27

Licensed under