LeNet ML System DMA Integration 深度解析
一句话概括
本模块是 Versal AIE-ML 异构计算系统中数据搬运的"交通枢纽"——它通过 HLS 实现的 DMA 内核,在 DDR 内存与 AI Engine-ML 阵列之间建立高速数据通道,解决了 LeNet CNN 推理 pipeline 中"数据从哪里来、到哪里去"的核心问题。想象一下机场的值机柜台:旅客(数据)从外部世界(DDR)进入,经过安检分流(PL 预处理),然后登上不同航班(AIE-ML 核心),最后取回行李(输出写回)。dma_hls 就是这个机场的值机和行李提取系统。
问题空间:为什么需要这个模块?
异构计算的"最后一公里"难题
Versal AIE-ML 架构包含三个关键计算域:
- PS(Processing System):ARM Cortex-A72,负责整体控制和任务调度
- PL(Programmable Logic):可编程逻辑,适合数据重排和预处理
- AIE-ML(AI Engine-ML):专用 AI 加速阵列,执行矩阵乘等密集计算
这三个域之间的数据传输是整个系统的瓶颈。AIE-ML 核心可以每周期执行数百次 MAC 运算,但如果数据供给跟不上,这些计算单元就会处于饥饿状态。
朴素的解决方案为何不行
一个 naive 的设计可能会让 PS 直接通过寄存器访问来搬运数据:
// 反模式:PS 轮询式数据搬运
for (int i = 0; i < data_size; i++) {
aie_input_reg[i] = ddr_buffer[i]; // 极度低效!
}
这种做法的问题:
- 带宽利用率低:PS 的内存访问无法充分利用 NoC(Network on Chip)的高带宽
- CPU 占用高:PS 被阻塞在数据搬运上,无法执行其他任务
- 延迟不可预测:软件轮询引入抖动,破坏实时性
设计洞察:专用硬件 DMA + 流水线并行
lenet_ml_system_dma_integration 采用硬件 DMA 引擎,通过 DATAFLOW pragma 实现双工并发传输:
#pragma HLS DATAFLOW
dma_mm2s(mem_rd, strm_out, mem_rd_size); // 读通道:DDR → AIE-ML
dma_s2mm(mem_wr, strm_in, mem_wr_size); // 写通道:AIE-ML → DDR
这就像双向高速公路—— inbound 和 outbound 流量互不干扰,同时利用 AXI4-Stream 的流式特性避免随机访问开销。
架构全景
HLS Kernel] LENET[lenet_kernel
RTL Kernel] end subgraph AIE["AI Engine-ML Array"] GRAPH[myGraph
5个核心: core01-05] end subgraph Memory["Memory Hierarchy"] DDR[DDR via NoC] BRAM[Block RAM] end APP -->|xrtBOAlloc/xrtRunStart| XRT XRT -->|控制| DMA XRT -->|graph run| GRAPH DMA <-->|AXI4-MM
mem_rd/mem_wr| DDR DMA <-->|AXI4-Stream
strm_out/strm_in| LENET LENET <-->|PLIO| GRAPH style DMA fill:#f9f,stroke:#333 style GRAPH fill:#bbf,stroke:#333
组件角色说明
| 组件 | 类型 | 职责 | 关键接口 |
|---|---|---|---|
dma_hls |
HLS Kernel | 数据搬运枢纽 | m_axi (DDR), axis (AIE-ML) |
lenet_kernel |
RTL Kernel | 输入预处理、MaxPool、数据重排 | AXI4-Stream |
myGraph |
ADF Graph | LeNet 推理计算图 | PLIO (64-bit) |
core01-05 |
AIE Kernel | 矩阵乘法、全连接层 | Buffer/Stream |
核心组件深度解析
1. dma_hls —— 数据搬运引擎
位于 design/pl_src/datamover/dma_hls.cpp,这是整个系统的 I/O 门户。
函数签名与接口契约
extern "C" void dma_hls(
volatile dint *mem_rd, // [IN] DDR 读基地址
volatile dint *mem_wr, // [OUT] DDR 写基地址
axi_stream &strm_out, // [OUT] 到 AIE-ML 的输出流
axi_stream &strm_in, // [IN] 从 AIE-ML 的输入流
int mem_rd_size, // 读数据量(64-bit words)
int mem_wr_size, // 写数据量(64-bit words)
int iterCnt // 迭代次数
)
内存所有权模型
| 指针参数 | 所有权 | 生命周期保证 | 备注 |
|---|---|---|---|
mem_rd |
Host (PS) 分配,DMA 只读借用 | 必须在 dma_hls 执行期间有效 |
volatile 修饰防止编译器优化掉内存访问 |
mem_wr |
Host (PS) 分配,DMA 写入借用 | 同上 | 调用者负责预分配足够空间 |
strm_out/in |
HLS 内部 FIFO,无所有权转移 | 由 hls::stream RAII 管理 |
深度由工具自动推断 |
时序与并发模型
for(int i = iterCnt; i; --i) {
#pragma HLS DATAFLOW
dma_mm2s(mem_rd, strm_out, mem_rd_size); // Stage 1
dma_s2mm(mem_wr, strm_in, mem_wr_size); // Stage 2
}
关键设计决策:DATAFLOW pragma 创建了两个并行的流水线阶段:
-
dma_mm2s(Memory-Mapped to Stream):- 使用
m_axi接口从 DDR burst 读取数据 - 通过
ap_axis<DW,0,0,0>封装为 AXI4-Stream 格式 PIPELINE II=1保证每个周期输出一个 64-bit 数据
- 使用
-
dma_s2mm(Stream to Memory-Mapped):- 从 AXI4-Stream 接收数据
- 通过
m_axi接口 burst 写入 DDR - 同样保持
II=1
为什么这样设计?
- 双工并发:读和写可以重叠执行,隐藏内存访问延迟
- 解耦生产-消费:
hls::stream作为异步 FIFO,允许 AIE-ML 以不同速率处理 - 确定性延迟:硬件实现的流水线,无软件调度开销
AXI 接口配置详解
#pragma HLS INTERFACE m_axi port=mem_rd depth=4096 offset=slave bundle=gmem0
#pragma HLS INTERFACE m_axi port=mem_wr depth=4096 offset=slave bundle=gmem1
#pragma HLS INTERFACE axis port=strm_in
#pragma HLS INTERFACE axis port=strm_out
#pragma HLS INTERFACE s_axilite port=... bundle=control
| Pragma | 含义 | 设计意图 |
|---|---|---|
m_axi bundle=gmem0/1 |
绑定到不同的 AXI4-Full 端口 | 读写分离,避免端口争用 |
depth=4096 |
指示最大 burst 长度 | 帮助 HLS 优化访存调度 |
offset=slave |
支持基地址动态配置 | Host 可以通过 s_axilite 设置实际地址 |
s_axilite bundle=control |
控制寄存器接口 | XRT 通过此接口配置 DMA 参数 |
2. 系统连接配置 (lenet_x1.cfg)
配置文件定义了 kernel 实例化和流连接拓扑:
nk=dma_hls:1:dma_hls_0 # 实例化 1 个 dma_hls,命名为 dma_hls_0
nk=lenet_kernel_1_0:1:lenet_kernel_0 # 实例化 lenet_kernel
# 数据流连接(source:destination)
stream_connect=dma_hls_0.strm_out:lenet_kernel_0.s_axis_ipr
stream_connect=lenet_kernel_0.m_axis_ipr:ai_engine_0.prod_in1
stream_connect=ai_engine_0.prod_out3:dma_hls_0.strm_in
连接拓扑解读:
DDR ──m_axi──► dma_hls_0 ──axis──► lenet_kernel_0 ──plio──► AIE-ML (core01)
▲ │
└──────── axis ─────────────────────────────────┘
(via prod_out3 ← core04)
这是一个典型的反馈环路结构:
- 前向路径:DDR → DMA → PL 预处理 → AIE-ML 计算
- 反向路径:AIE-ML 最终输出 → DMA → DDR
3. Host 应用控制流程 (lenet_aie_app.cpp)
Host 代码展示了如何协调 DMA 和 AIE-ML Graph 的执行:
// 1. 分配 DDR 缓冲区
xrtBufferHandle in_bohdl = xrtBOAlloc(dhdl, input_size_in_bytes, 0, 0);
xrtBufferHandle out_bohdl = xrtBOAlloc(dhdl, output_size_in_bytes, 0, 0);
// 2. 打开 DMA kernel 并设置参数
xrtKernelHandle dmahls_khdl = xrtPLKernelOpen(dhdl, top->m_header.uuid, "dma_hls:{dma_hls_0}");
xrtRunHandle dmahls_rhdl = xrtRunOpen(dmahls_khdl);
xrtRunSetArg(dmahls_rhdl, 0, in_bohdl); // mem_rd
xrtRunSetArg(dmahls_rhdl, 1, out_bohdl); // mem_wr
xrtRunSetArg(dmahls_rhdl, 4, INPUT_SIZE); // mem_rd_size
xrtRunSetArg(dmahls_rhdl, 5, OUTPUT_SIZE); // mem_wr_size
xrtRunSetArg(dmahls_rhdl, 6, iterCnt); // 迭代次数
// 3. 启动 DMA(非阻塞)
xrtRunStart(dmahls_rhdl);
// 4. 启动 AIE-ML Graph
adf::registerXRT(dhdl, top->m_header.uuid);
auto graphHandle = xrtGraphOpen(dhdl, top->m_header.uuid, "g");
xrtGraphRun(graphHandle, GRAPH_ITER_CNT);
// 5. 等待 DMA 完成(同步点)
xrtRunWait(dmahls_rhdl);
执行时序关键点:
- DMA 先启动,确保数据通路就绪
- Graph 随后启动,开始消费输入数据
xrtRunWait是同步屏障,确保所有数据回写到 DDR 后才进行结果校验
数据流完整追踪
以单张图像的 LeNet 推理为例,追踪数据从 DDR 出发再回到 DDR 的完整旅程:
Phase 1: 输入加载(Input Ingress)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DDR[input.h] ──m_axi(gmem0)──► dma_hls.mm2s ──axis(strm_out)──►
lenet_kernel.s_axis_ipr ──►
PL 预处理(数据重排)──►
lenet_kernel.m_axis_ipr ──plio──►
core01.in[0] (AIE-ML Tile 8,0)
Phase 2: AIE-ML 计算流水线(Compute Pipeline)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
core01 (卷积层1) ──► tensor01 (shared_buffer) ──► core02 (卷积层2)
│
▼
lenet_kernel (M1R1: MaxPool+重排)
│
▼
core03 (FC1 前半) ──┐ core05 (FC1 后半)
│ │
└────────► core04 (FC2+Softmax) ◄─────┘
│
▼
tensor03 ──► prod_out3
Phase 3: 输出回写(Output Egress)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ai_engine_0.prod_out3 ──plio──► lenet_kernel ──axis──►
dma_hls_0.strm_in ──m_axi(gmem1)──►
DDR[out_bomapped]
吞吐量分析
基于代码中的关键参数:
| 参数 | 值 | 含义 |
|---|---|---|
DW = 64 |
64 bits | AXI4-Stream 数据宽度 |
II = 1 |
1 cycle | 初始化间隔(Initiation Interval) |
plio_64_bits |
64 bits | PLIO 接口宽度 |
假设时钟频率为 300 MHz:
- 理论峰值带宽 = 64 bits × 300 MHz = 19.2 Gbps = 2.4 GB/s
- 对于 100 张图像的 batch,输入大小为 0x80 × 100 = 12.8 KB,输出为 0x10 × 100 = 2.56 KB
设计决策与权衡
1. HLS vs RTL:为什么选择 HLS 实现 DMA?
| 方案 | 优势 | 劣势 | 本模块选择 |
|---|---|---|---|
HLS (dma_hls) |
开发快速、易维护、pragma 驱动优化 | 对底层控制稍弱 | ✅ 选用 |
RTL (lenet_kernel) |
极致性能、精确时序控制 | 开发周期长、难调试 | 用于计算密集型预处理 |
决策理由:DMA 是标准的数据搬运模式,HLS 的 DATAFLOW 和 PIPELINE pragma 足以生成高效硬件,且便于后续调整 buffer depth 和接口参数。
2. 双工并发 vs 顺序执行
// 选项 A:顺序执行(未采用)
for(...) {
dma_mm2s(...); // 读完再写
dma_s2mm(...);
}
// 选项 B:双工并发(采用)
for(...) {
#pragma HLS DATAFLOW
dma_mm2s(...); // 同时进行
dma_s2mm(...);
}
选择双工并发的理由:
- LeNet 是流水线结构,前一帧的输出可以和后一帧的输入重叠
- 隐藏 DDR 访问延迟:当 AIE-ML 处理当前帧时,DMA 可以预取下一帧
- 资源开销可接受:
hls::stream的 FIFO 深度由工具自动平衡
3. 共享 Buffer 的 Tiling 策略
在 graph.h 中,tensor01/02/03 使用 shared_buffer 配合复杂的 tiling 配置:
tensor01 = shared_buffer<int32_t>::create({ 2,4,144 }, 1, 1);
write_access(tensor01.in[0]) = tiling({
.buffer_dimension = { 2, 4, 144},
.tiling_dimension = { 2, 4, 144},
.offset = { 0, 0, 0}
});
read_access(tensor01.out[0]) = tiling({
.buffer_dimension = { 2, 4, 144},
.tiling_dimension = { 1, 4, 1}, // 关键:读取粒度变小
...
});
设计洞察:
write_access使用大 tile(2×4×144),最大化 burst 效率read_access使用小 tile(1×4×1),匹配 AIE-ML 核心的向量处理宽度- 这种不对称 tiling 是内存层次优化的关键技巧
4. 耦合与边界
紧耦合区域(修改需谨慎):
lenet_x1.cfg中的stream_connect定义了拓扑,改名需要同步修改多处dma_hls的参数顺序(0: in, 1: out, 4: size_in, 5: size_out, 6: iter)与 Host 代码硬编码对应
松耦合区域(易于扩展):
iterCnt允许动态调整迭代次数,无需重新编译硬件- AIE-ML Graph 的
runtime<ratio>可以独立调整各核心的计算比例
新贡献者须知:陷阱与最佳实践
常见错误
-
Size 单位混淆
// 错误:误以为是以字节为单位 xrtRunSetArg(dmahls_rhdl, 4, INPUT_SIZE * sizeof(uint32_t)); // ❌ // 正确:INPUT_SIZE 已经是 64-bit word 计数 xrtRunSetArg(dmahls_rhdl, 4, INPUT_SIZE); // ✅ -
忽略
volatile语义// dma_hls.cpp 中 mem_rd/mem_wr 标记为 volatile // 如果去掉,HLS 可能优化掉看似"冗余"的内存访问 -
DATAFLOW 区域的循环依赖
// 危险:如果在 DATAFLOW 区域内引入循环依赖,HLS 会报错或生成错误硬件 #pragma HLS DATAFLOW funcA(out1, in1); // out1 是 funcB 的输入 funcB(out2, out1); // 这是合法的流水线 funcC(out1, out2); // ❌ out1 既是输出又是输入,形成反馈
调试技巧
-
启用 HLS 仿真验证
# 使用 dma_hls_test.cpp 进行 C/RTL 协同仿真 cd design/pl_src/datamover vitis_hls -f script.tcl # 检查生成的 RTL 行为 -
观察 AXI4-Stream 的
last信号// dma_hls.cpp 中设置了 s.last 标志 if (i == size-1) s.last = 1; // 这是 AIE-ML 判断 packet 边界的关键信号 -
使用 XRT 的 Profiling 功能
// 在 host 代码中添加时间戳 auto start = std::chrono::high_resolution_clock::now(); xrtRunWait(dmahls_rhdl); auto end = std::chrono::high_resolution_clock::now();
扩展指南
添加新的数据通路:
- 在
globals.h中定义新的数据类型和常量 - 在
dma_hls.cpp中添加新的 mm2s/s2mm 函数对 - 更新
lenet_x1.cfg的stream_connect - 在 Host 代码中分配对应的 buffer 并设置参数
调整 Buffer Depth:
// 如果 AIE-ML 处理速度波动较大,可以增加 FIFO 深度
#pragma HLS STREAM variable=strm_out depth=512 // 默认由工具推断
相关模块参考
- AIE_ML_Design_Graphs - AIE-ML Graph 的整体架构
- prime_factor_fft_pipeline_graphs - 另一个使用 DMA 的示例
- mnist_convnet_layer_and_full_network_graphs - 类似的 CNN 实现参考
- AIE_ML_PL_HLS_Integration - PL 与 HLS 集成的通用模式
总结
lenet_ml_system_dma_integration 模块虽然代码量不大,但承载了整个 LeNet 系统的数据生命线。它的设计体现了异构计算的一个核心原则:让专业的硬件做专业的事——PS 负责控制、PL 负责数据搬运和预处理、AIE-ML 负责密集计算。理解这个模块的数据流和控制流,是掌握整个 Versal AIE-ML 系统设计范式的关键一步。