🏠

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 架构包含三个关键计算域:

  1. PS(Processing System):ARM Cortex-A72,负责整体控制和任务调度
  2. PL(Programmable Logic):可编程逻辑,适合数据重排和预处理
  3. 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 的流式特性避免随机访问开销。


架构全景

flowchart TB subgraph Host["Host (ARM A72)"] APP[lenet_aie_app.cpp] XRT[XRT Runtime] end subgraph PL["Programmable Logic (PL)"] DMA[dma_hls
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 创建了两个并行的流水线阶段:

  1. dma_mm2s(Memory-Mapped to Stream):

    • 使用 m_axi 接口从 DDR burst 读取数据
    • 通过 ap_axis<DW,0,0,0> 封装为 AXI4-Stream 格式
    • PIPELINE II=1 保证每个周期输出一个 64-bit 数据
  2. 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 的 DATAFLOWPIPELINE 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> 可以独立调整各核心的计算比例

新贡献者须知:陷阱与最佳实践

常见错误

  1. Size 单位混淆

    // 错误:误以为是以字节为单位
    xrtRunSetArg(dmahls_rhdl, 4, INPUT_SIZE * sizeof(uint32_t));  // ❌
    
    // 正确:INPUT_SIZE 已经是 64-bit word 计数
    xrtRunSetArg(dmahls_rhdl, 4, INPUT_SIZE);  // ✅
    
  2. 忽略 volatile 语义

    // dma_hls.cpp 中 mem_rd/mem_wr 标记为 volatile
    // 如果去掉,HLS 可能优化掉看似"冗余"的内存访问
    
  3. DATAFLOW 区域的循环依赖

    // 危险:如果在 DATAFLOW 区域内引入循环依赖,HLS 会报错或生成错误硬件
    #pragma HLS DATAFLOW
    funcA(out1, in1);   // out1 是 funcB 的输入
    funcB(out2, out1);  // 这是合法的流水线
    funcC(out1, out2);  // ❌ out1 既是输出又是输入,形成反馈
    

调试技巧

  1. 启用 HLS 仿真验证

    # 使用 dma_hls_test.cpp 进行 C/RTL 协同仿真
    cd design/pl_src/datamover
    vitis_hls -f script.tcl  # 检查生成的 RTL 行为
    
  2. 观察 AXI4-Stream 的 last 信号

    // dma_hls.cpp 中设置了 s.last 标志
    if (i == size-1) s.last = 1;
    // 这是 AIE-ML 判断 packet 边界的关键信号
    
  3. 使用 XRT 的 Profiling 功能

    // 在 host 代码中添加时间戳
    auto start = std::chrono::high_resolution_clock::now();
    xrtRunWait(dmahls_rhdl);
    auto end = std::chrono::high_resolution_clock::now();
    

扩展指南

添加新的数据通路

  1. globals.h 中定义新的数据类型和常量
  2. dma_hls.cpp 中添加新的 mm2s/s2mm 函数对
  3. 更新 lenet_x1.cfgstream_connect
  4. 在 Host 代码中分配对应的 buffer 并设置参数

调整 Buffer Depth

// 如果 AIE-ML 处理速度波动较大,可以增加 FIFO 深度
#pragma HLS STREAM variable=strm_out depth=512  // 默认由工具推断

相关模块参考


总结

lenet_ml_system_dma_integration 模块虽然代码量不大,但承载了整个 LeNet 系统的数据生命线。它的设计体现了异构计算的一个核心原则:让专业的硬件做专业的事——PS 负责控制、PL 负责数据搬运和预处理、AIE-ML 负责密集计算。理解这个模块的数据流和控制流,是掌握整个 Versal AIE-ML 系统设计范式的关键一步。

On this page