Host Memory Connectivity Scaling 模块深度解析
一句话概括
本模块演示了如何通过**主机内存直连(Host Memory Access)**技术,将 FPGA 加速器从传统的"DDR 中转站"模式解放出来,让内核直接访问主机内存——这就像把工厂从"必须先把原料运到本地仓库才能开工"转变为"直接从供应商处取货生产",虽然单次取货路程变长了,但省去了频繁的搬运环节,整体吞吐量反而提升。
问题空间:为什么需要这个模块?
传统 DDR 模式的瓶颈
在典型的 FPGA 加速应用中,数据流遵循以下路径:
主机内存 → XDMA 引擎 → FPGA DDR → 计算内核 → FPGA DDR → XDMA 引擎 → 主机内存
这个过程存在几个隐形成本:
- CPU 负担重:每次数据传输都需要主机 CPU 参与协调 XDMA 操作
- 延迟叠加:数据需要在 DDR 和主机内存之间来回搬运
- 资源占用:XDMA 逻辑消耗宝贵的 FPGA 片上资源
- 并行度受限:主机同时能处理的 DMA 请求数量有限
Host Memory 模式的突破
AMD/Xilinx 的部分平台(如 U50-NoDMA)支持绕过 XDMA,让内核通过 PCIe 直接访问主机内存。这带来了新的可能性:
主机内存 ←→ 计算内核(直接访问)
关键洞察:虽然访问主机内存的延迟比访问板载 DDR 高(约 ~1μs vs ~100ns),但如果能让主机 CPU 从"搬运工"角色中解放出来,转而专注于"调度员"角色,整体系统吞吐量可能反而提升。
核心抽象与心智模型
类比:餐厅后厨的两种运作模式
| 模式 | 类比 | 特点 |
|---|---|---|
| DDR 模式 | 中央厨房 + 分店仓库 | 食材先运到分店仓库,厨师从仓库取料。仓库周转快,但需要专人负责进货 |
| Host Memory 模式 | 中央厨房直供 | 厨师直接从中央厨房取料,路途远一些,但省去了进货环节,厨师可以更专注于烹饪 |
关键抽象
-
cl_mem_ext_ptr_t扩展指针- 标志位
XCL_MEM_EXT_HOST_ONLY告诉运行时:"这个缓冲区位于主机内存" - 这是从 DDR 模式切换到 Host Memory 模式的唯一代码变更点
- 标志位
-
缓存同步 vs 数据搬运
- DDR 模式:
clEnqueueMigrateMemObjects触发实际的 PCIe DMA 传输 - Host Memory 模式:同样的 API 调用变成轻量级的缓存一致性操作(cache invalidate/flush)
- DDR 模式:
-
计算单元(CU)池
- 15 个
vadd内核实例分布在 4 个 SLR(Super Logic Region)上 - 每个 CU 独立运行,通过回调机制实现"完成即重启"的无限循环
- 15 个
架构与数据流
系统拓扑图
graph TB
subgraph Host["主机端"]
H[Host Application
host.cpp / host_hm.cpp] HM[Host Memory Buffer
XCL_MEM_EXT_HOST_ONLY] Q[OpenCL Command Queue
Out-of-Order] end subgraph FPGA["FPGA 器件"] subgraph SLR0["SLR0"] CU1[vadd_1]<-->CU2[vadd_2]<-->CU3[vadd_3]<-->CU4[vadd_4] end subgraph SLR1["SLR1"] CU5[vadd_5]<-->CU6[vadd_6]<-->CU7[vadd_7] end subgraph SLR2["SLR2"] CU8[vadd_8]<-->CU9[vadd_9]<-->CU10[vadd_10]<-->CU11[vadd_11] end subgraph SLR3["SLR3"] CU12[vadd_12]<-->CU13[vadd_13]<-->CU14[vadd_14]<-->CU15[vadd_15] end end subgraph Memory["内存子系统"] DDR0[DDR Bank 0] DDR1[DDR Bank 1] DDR2[DDR Bank 2] DDR3[DDR Bank 3] HOST[Host Memory
via PCIe] end H --> Q Q --> CU1 & CU2 & CU3 & CU4 & CU5 & CU6 & CU7 & CU8 & CU9 & CU10 & CU11 & CU12 & CU13 & CU14 & CU15 %% DDR 模式连接 CU1 -.->|link.cfg| DDR0 CU5 -.->|link.cfg| DDR1 CU8 -.->|link.cfg| DDR2 CU12 -.->|link.cfg| DDR3 %% Host Memory 模式连接 CU1 -.->|link_hm.cfg| HOST CU5 -.->|link_hm.cfg| HOST CU8 -.->|link_hm.cfg| HOST CU12 -.->|link_hm.cfg| HOST H -.->|Host Memory 模式| HM
host.cpp / host_hm.cpp] HM[Host Memory Buffer
XCL_MEM_EXT_HOST_ONLY] Q[OpenCL Command Queue
Out-of-Order] end subgraph FPGA["FPGA 器件"] subgraph SLR0["SLR0"] CU1[vadd_1]<-->CU2[vadd_2]<-->CU3[vadd_3]<-->CU4[vadd_4] end subgraph SLR1["SLR1"] CU5[vadd_5]<-->CU6[vadd_6]<-->CU7[vadd_7] end subgraph SLR2["SLR2"] CU8[vadd_8]<-->CU9[vadd_9]<-->CU10[vadd_10]<-->CU11[vadd_11] end subgraph SLR3["SLR3"] CU12[vadd_12]<-->CU13[vadd_13]<-->CU14[vadd_14]<-->CU15[vadd_15] end end subgraph Memory["内存子系统"] DDR0[DDR Bank 0] DDR1[DDR Bank 1] DDR2[DDR Bank 2] DDR3[DDR Bank 3] HOST[Host Memory
via PCIe] end H --> Q Q --> CU1 & CU2 & CU3 & CU4 & CU5 & CU6 & CU7 & CU8 & CU9 & CU10 & CU11 & CU12 & CU13 & CU14 & CU15 %% DDR 模式连接 CU1 -.->|link.cfg| DDR0 CU5 -.->|link.cfg| DDR1 CU8 -.->|link.cfg| DDR2 CU12 -.->|link.cfg| DDR3 %% Host Memory 模式连接 CU1 -.->|link_hm.cfg| HOST CU5 -.->|link_hm.cfg| HOST CU8 -.->|link_hm.cfg| HOST CU12 -.->|link_hm.cfg| HOST H -.->|Host Memory 模式| HM
执行流程对比
DDR 模式(host.cpp + link.cfg)
1. 初始化阶段:
- clCreateBuffer(DDR) → 在 FPGA DDR 上分配缓冲区
- clEnqueueMapBuffer → 获取主机可写的映射指针
- memcpy → 填充输入数据
- clEnqueueMigrateMemObjects(0) → 主机→DDR DMA 传输
2. 执行阶段(每个 CU 迭代):
- clEnqueueMigrateMemObjects(0) → 写输入数据到 DDR
- clEnqueueTask → 启动内核
- clEnqueueMigrateMemObjects(CL_MIGRATE_MEM_OBJECT_HOST) → 读结果回主机
- 回调触发下一次迭代
3. 关键特征:
- 每次迭代涉及 2 次显式 DMA 传输
- 主机 CPU 参与数据传输协调
- Profile 报告中有 "Host Transfer" 章节
Host Memory 模式(host_hm.cpp + link_hm.cfg)
1. 初始化阶段:
- cl_mem_ext_ptr_t.flags = XCL_MEM_EXT_HOST_ONLY
- clCreateBuffer(...|CL_MEM_EXT_PTR_XILINX) → 在主机内存分配缓冲区
- clEnqueueMapBuffer → 获取直接映射指针(无拷贝)
- memcpy → 填充数据(直接写入主机内存)
- clEnqueueMigrateMemObjects → 轻量级缓存同步
2. 执行阶段(每个 CU 迭代):
- clEnqueueMigrateMemObjects → 缓存失效(invalidate)
- clEnqueueTask → 内核直接读写主机内存
- clEnqueueMigrateMemObjects → 缓存刷新(flush)
- 回调触发下一次迭代
3. 关键特征:
- 无显式 DMA 传输,只有缓存同步
- 主机 CPU 仅负责提交命令队列
- Profile 报告中无 "Host Transfer" 章节
- 显示 "Host Memory Synchronization"
关键设计决策与权衡
1. 15 个 CU 的分布策略
# link.cfg / link_hm.cfg
nk=vadd:15:vadd_1.vadd_2...vadd_15
slr=vadd_1:SLR0
slr=vadd_2:SLR0
slr=vadd_3:SLR0
slr=vadd_4:SLR0
slr=vadd_5:SLR1
...
| SLR | CU 数量 | 说明 |
|---|---|---|
| SLR0 | 4 | 靠近 PCIe 接口,延迟最低 |
| SLR1 | 3 | 中间位置 |
| SLR2 | 4 | 中间位置 |
| SLR3 | 4 | 远离 PCIe,但仍有足够资源 |
设计意图:
- 最大化利用 U250 的四个 SLR 资源
- SLR0 放置最多 CU,因为最靠近 PCIe 接口,访问主机内存延迟最低
- 平衡负载,避免某个 SLR 成为瓶颈
2. 内存连接配置的对比
DDR 模式(link.cfg)
sp=vadd_1.m_axi_gmem:DDR[0]
sp=vadd_2.m_axi_gmem:DDR[0]
sp=vadd_3.m_axi_gmem:DDR[0]
sp=vadd_4.m_axi_gmem:DDR[0]
sp=vadd_5.m_axi_gmem:DDR[1]
...
sp=vadd_15.m_axi_gmem:DDR[3]
特点:
- 15 个 CU 分散连接到 4 个 DDR Bank
- 每个 DDR Bank 服务 3-4 个 CU
- 需要仔细规划以避免 DDR Bank 争用
Host Memory 模式(link_hm.cfg)
sp=vadd_1.m_axi_gmem:HOST[0]
sp=vadd_2.m_axi_gmem:HOST[0]
...
sp=vadd_15.m_axi_gmem:HOST[0]
特点:
- 所有 15 个 CU 共享同一个 HOST 内存通道
- 配置更简单,无需手动分区
- 依赖 PCIe 交换结构的带宽能力
3. 性能权衡分析
| 指标 | DDR 模式 | Host Memory 模式 | 原因 |
|---|---|---|---|
| 单 CU 执行时间 | ~1ms | ~1.2ms | 访问主机内存延迟更高 |
| 20 秒总执行次数 | ~40,000 | ~53,000 | 主机 CPU 解放,提交更多并行请求 |
| 有效吞吐 | ~8 GB/s | ~10.7 GB/s | 更高的 CU 利用率 |
| 主机 CPU 负载 | 高(参与 DMA) | 低(仅提交命令) | 缓存同步 vs 数据搬运 |
| 并行请求行数 | 4 行 | 10 行 | 更高的命令队列并行度 |
关键洞察:
- Host Memory 模式下,单个 CU 变慢了(~20%),但整体系统变快了(~33%)
- 这是因为系统瓶颈从"数据传输"转移到了"计算能力"
- 类似多车道高速公路:单辆车速度可能略降,但整体通行能力提升
4. 为什么选择 Out-of-Order 命令队列
cl_command_queue queue = clCreateCommandQueue(
context, device,
CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE, // 关键标志
&err
);
设计理由:
- 允许运行时并行调度多个独立的 CU 执行
- 配合事件依赖链(
write_event→ev_kernel_done→read_done)保证正确性 - 最大化 FPGA 资源利用率,让所有 15 个 CU 尽可能同时忙碌
代码结构详解
文件组织
reference-files/
├── src/
│ ├── kernel.cpp # HLS 向量加法内核
│ ├── host.cpp # DDR 模式主机代码
│ ├── host_hm.cpp # Host Memory 模式主机代码
│ ├── link.cfg # DDR 连接配置
│ └── link_hm.cfg # Host Memory 连接配置
├── Makefile # 构建脚本
├── xrt.ini # XRT 运行时配置
└── description.json # 教程描述
内核代码要点(kernel.cpp)
#pragma HLS INTERFACE m_axi port=in1 bundle=gmem \
num_write_outstanding=32 max_write_burst_length=64 \
num_read_outstanding=32 max_read_burst_length=64
HLS 优化策略:
- 512-bit 数据宽度:最大化 AXI 总线带宽利用率
- ** outstanding 事务 = 32**:允许最多 32 个未完成的事务,隐藏延迟
- 最大突发长度 = 64:最大化每次总线事务的数据量
- 双缓冲(BUFFER_SIZE = 128):重叠计算和数据读取
主机代码关键差异
| 方面 | host.cpp (DDR) |
host_hm.cpp (Host Memory) |
|---|---|---|
| 缓冲区创建 | clCreateBuffer(context, CL_MEM_READ_ONLY, bytes, nullptr, &err) |
clCreateBuffer(context, CL_MEM_READ_ONLY|CL_MEM_EXT_PTR_XILINX, bytes, &host_buffer_ext, &err) |
| 扩展指针 | 无 | host_buffer_ext.flags = XCL_MEM_EXT_HOST_ONLY |
| 数据传输 | 显式 DMA | 隐式缓存同步 |
新贡献者注意事项
1. 环境准备陷阱
必须在运行前启用主机内存:
# 一次性配置(重启后失效)
sudo /opt/xilinx/xrt/bin/xbutil host_mem --enable --size 1G
# 验证配置
cat /proc/meminfo | grep CMA
常见错误:忘记启用主机内存会导致 clCreateBuffer 失败或段错误。
2. 性能测试的注意事项
"性能数字可能因主机服务器而异,本示例中的数据仅供参考。"
- 不同服务器的 PCIe 拓扑、NUMA 配置、内存带宽都会影响结果
- 建议在目标部署环境中进行实际性能评估
- 关注相对提升比例而非绝对数值
3. 回调驱动的无限循环模式
void run() {
++runs;
// ... 提交任务 ...
clSetEventCallback(read_done, CL_COMPLETE, &kernel_done, this);
}
static void kernel_done(cl_event event, cl_int status, void* data) {
clReleaseEvent(event);
reinterpret_cast<job_type*>(data)->done(); // 递归触发
}
void done() {
if (!stop) run(); // 停止标志控制退出
}
理解要点:
- 这不是传统的"for 循环"模式,而是事件驱动的异步模式
stop全局变量用于优雅退出- 注意栈深度:长时间运行不会导致栈溢出,因为每次回调都在新的调用帧
4. 内存所有权模型
struct job_type {
cl_kernel krnl; // OpenCL 对象,由 clReleaseKernel 释放
cl_mem in1, in2, io; // OpenCL 缓冲区对象
int* in1_mapped; // 主机映射指针,由 clEnqueueUnmapMemObject 释放
~job_type() {
clReleaseKernel(krnl);
// 必须先 unmap 再 release mem object
clEnqueueUnmapMemObject(queue, in1, in1_mapped, ...);
clReleaseMemObject(in1);
}
};
释放顺序至关重要:
- 先
clEnqueueUnmapMemObject解除映射 - 等待 unmap 事件完成
- 最后
clReleaseMemObject释放缓冲区
5. 隐含的假设与约束
- 固定数据大小:
number_of_elements = 1024*512,每个缓冲区 2MB - 简化验证:实际代码省略了结果验证以聚焦性能测试
- 无错误恢复:遇到错误直接抛出异常终止程序
- 单设备假设:代码只使用第一个找到的加速器设备
与其他模块的关系
上游依赖
- vivado_implementation_control_and_host_memory_setup:提供主机内存的基础平台配置知识
下游关联
- hbm_kernel_data_type_definitions:当需要更高带宽时,可考虑 HBM(高带宽内存)作为下一步优化方向
平行参考
- multi_compute_unit_dispatch_and_host_control:更通用的多 CU 调度模式
总结
Host Memory Connectivity Scaling 模块展示了 FPGA 加速的一个高级优化技巧:通过改变数据访问模式来重新平衡系统负载。核心收获:
- 延迟 ≠ 吞吐:更高的单操作延迟不一定意味着更低的系统吞吐
- 解放 CPU:让主机专注于调度而非搬运,往往能带来整体收益
- 配置极简:从 DDR 迁移到 Host Memory 只需修改链接配置和缓冲区创建标志
- 适用场景:适合计算密集、数据重用少、主机 CPU 成为瓶颈的应用
这是一个典型的"反直觉"优化案例——看似更慢的路径,在系统层面却更快。