🏠

Host Memory Connectivity Scaling 模块深度解析

一句话概括

本模块演示了如何通过**主机内存直连(Host Memory Access)**技术,将 FPGA 加速器从传统的"DDR 中转站"模式解放出来,让内核直接访问主机内存——这就像把工厂从"必须先把原料运到本地仓库才能开工"转变为"直接从供应商处取货生产",虽然单次取货路程变长了,但省去了频繁的搬运环节,整体吞吐量反而提升。


问题空间:为什么需要这个模块?

传统 DDR 模式的瓶颈

在典型的 FPGA 加速应用中,数据流遵循以下路径:

主机内存 → XDMA 引擎 → FPGA DDR → 计算内核 → FPGA DDR → XDMA 引擎 → 主机内存

这个过程存在几个隐形成本:

  1. CPU 负担重:每次数据传输都需要主机 CPU 参与协调 XDMA 操作
  2. 延迟叠加:数据需要在 DDR 和主机内存之间来回搬运
  3. 资源占用:XDMA 逻辑消耗宝贵的 FPGA 片上资源
  4. 并行度受限:主机同时能处理的 DMA 请求数量有限

Host Memory 模式的突破

AMD/Xilinx 的部分平台(如 U50-NoDMA)支持绕过 XDMA,让内核通过 PCIe 直接访问主机内存。这带来了新的可能性:

主机内存 ←→ 计算内核(直接访问)

关键洞察:虽然访问主机内存的延迟比访问板载 DDR 高(约 ~1μs vs ~100ns),但如果能让主机 CPU 从"搬运工"角色中解放出来,转而专注于"调度员"角色,整体系统吞吐量可能反而提升。


核心抽象与心智模型

类比:餐厅后厨的两种运作模式

模式 类比 特点
DDR 模式 中央厨房 + 分店仓库 食材先运到分店仓库,厨师从仓库取料。仓库周转快,但需要专人负责进货
Host Memory 模式 中央厨房直供 厨师直接从中央厨房取料,路途远一些,但省去了进货环节,厨师可以更专注于烹饪

关键抽象

  1. cl_mem_ext_ptr_t 扩展指针

    • 标志位 XCL_MEM_EXT_HOST_ONLY 告诉运行时:"这个缓冲区位于主机内存"
    • 这是从 DDR 模式切换到 Host Memory 模式的唯一代码变更点
  2. 缓存同步 vs 数据搬运

    • DDR 模式:clEnqueueMigrateMemObjects 触发实际的 PCIe DMA 传输
    • Host Memory 模式:同样的 API 调用变成轻量级的缓存一致性操作(cache invalidate/flush)
  3. 计算单元(CU)池

    • 15 个 vadd 内核实例分布在 4 个 SLR(Super Logic Region)上
    • 每个 CU 独立运行,通过回调机制实现"完成即重启"的无限循环

架构与数据流

系统拓扑图

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

执行流程对比

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_eventev_kernel_doneread_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);
    }
};

释放顺序至关重要

  1. clEnqueueUnmapMemObject 解除映射
  2. 等待 unmap 事件完成
  3. 最后 clReleaseMemObject 释放缓冲区

5. 隐含的假设与约束

  • 固定数据大小number_of_elements = 1024*512,每个缓冲区 2MB
  • 简化验证:实际代码省略了结果验证以聚焦性能测试
  • 无错误恢复:遇到错误直接抛出异常终止程序
  • 单设备假设:代码只使用第一个找到的加速器设备

与其他模块的关系

上游依赖

下游关联

平行参考


总结

Host Memory Connectivity Scaling 模块展示了 FPGA 加速的一个高级优化技巧:通过改变数据访问模式来重新平衡系统负载。核心收获:

  1. 延迟 ≠ 吞吐:更高的单操作延迟不一定意味着更低的系统吞吐
  2. 解放 CPU:让主机专注于调度而非搬运,往往能带来整体收益
  3. 配置极简:从 DDR 迁移到 Host Memory 只需修改链接配置和缓冲区创建标志
  4. 适用场景:适合计算密集、数据重用少、主机 CPU 成为瓶颈的应用

这是一个典型的"反直觉"优化案例——看似更慢的路径,在系统层面却更快。

On this page