🏠

Independent Graphs Composition(独立图组合)模块深度解析

一句话概括

本模块展示了如何在 AMD Versal AI Engine 上将一个大型系统拆分编译为多个独立的硬件分区,让每个团队可以独立开发、仿真和验证自己的子系统,最后通过 V++ 链接器将它们无缝集成为一个完整的可执行系统——就像把多个独立开发的乐高模块拼接成一个复杂的机械装置。


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

传统单体设计的痛点

在传统的 AI Engine 开发流程中,整个系统的所有计算图(graph)必须在一次编译中完成:

  1. 协作困难:多个团队同时修改同一个代码库,冲突频繁
  2. 编译时间爆炸:任何小改动都需要重新编译整个系统
  3. 验证瓶颈:无法单独验证某个子模块,必须等整个系统就绪
  4. 集成风险:问题只能在最后阶段发现,修复成本极高

独立分区方案的解决思路

想象一个城市规划项目:与其让所有建筑师挤在一个大房间里画一张巨型蓝图,不如把城市划分为若干个街区,每个团队负责一个街区的详细设计,最后由城市规划部门确保街区之间的道路和管线正确连接。

本模块正是这一思想在硬件设计中的实现:

  • pr0_gmio: GMIO 接口分区(第6列),处理全局内存 I/O
  • pr1_rtp: RTP(运行时参数)分区(第8列),支持动态参数更新
  • pr2_perf: 高性能流处理分区(第10-11列),展示流水线优化

核心抽象与心智模型

类比:模块化公寓楼

把 AI Engine 阵列想象成一栋公寓楼:

概念 公寓楼类比 技术含义
Partition(分区) 独立的楼层/单元 占据特定 AIE 列范围的独立编译单元
Graph(计算图) 楼层内的房间布局 定义内核(kernel)及其连接关系
PLIO/GMIO 楼层对外的门窗 与外部世界(PL/PS/DDR)的数据通道
Stream Connect 楼层间的连廊 不同分区之间的数据流连接
libadf.a 预制好的楼层模块 每个分区编译生成的静态库

关键设计原则

┌─────────────────────────────────────────────────────────────┐
│                    Versal Device Floorplan                   │
├─────────┬─────────┬─────────┬───────────────────────────────┤
│  pr0    │  pr1    │  pr2    │        PL Region              │
│ (col 6) │ (col 8) │(col 10-11)│  (Programmable Logic)       │
│         │         │         │                               │
│ ┌─────┐ │ ┌─────┐ │ ┌─────┐ │  ┌─────────┐  ┌─────────┐    │
│ │GMIO │ │ │RTP  │ │ │Perf │ │  │ datagen │  │  mm2s   │    │
│ │In/Out│ │ │Port │ │ │Pipe │ │  └────┬────┘  └────┬────┘    │
│ └──┬──┘ │ └──┬──┘ │ └──┬──┘ │       │            │          │
│    │    │    │    │    │    │       ▼            ▼          │
│  DDR   PL Stream PL Stream  │  ┌─────────────────────────┐   │
│ Access  Connect  Connect    │  │      system.cfg         │   │
│                             │  │  stream_connect=...     │   │
│  weighted_sum    vect_add   │  │  (Connectivity Matrix)  │   │
│  (FIR-like)     (add const) │  └─────────────────────────┘   │
│                             │                                │
└─────────────────────────────┴────────────────────────────────┘

架构详解与数据流

整体架构图

flowchart TB subgraph PS["Processing System (ARM)"] HOST0[host0.exe
控制 pr0] HOST1[host1.exe
控制 pr1] HOST2[host2.exe
控制 pr2] HOST_ALL[host_all.exe
统一控制] end subgraph PL["Programmable Logic (FPGA)"] DATAGEN[datagen
数据生成器] MM2S[mm2s
Memory-to-Stream] S2MM_1[s2mm_1
Stream-to-Memory] S2MM_2[s2mm_2
Stream-to-Memory] end subgraph AIE["AI Engine Array"] subgraph PR0["pr0 (col 6)
GMIO Partition"] K0[weighted_sum
Kernel] GMIO_IN[gmioIn] GMIO_OUT[gmioOut] end subgraph PR1["pr1 (col 8)
RTP Partition"] K1[vect_add
Kernel] PLIO_IN[Datain0] PLIO_OUT[Dataout0] RTP[value
Runtime Param] end subgraph PR2["pr2 (col 10-11)
Performance Partition"] K2[aie_dest1] K3[aie_dest2] PLIO_IN2[Datain0] PLIO_OUT2[Dataout0] end end DDR[(DDR Memory)] %% Data flows HOST0 <-->|GMIO| GMIO_IN GMIO_IN --> K0 --> GMIO_OUT GMIO_OUT <-->|GMIO| HOST0 DATAGEN -->|stream| PLIO_IN PLIO_IN --> K1 --> PLIO_OUT PLIO_OUT -->|stream| S2MM_1 HOST1 -->|update RTP| RTP RTP -.->|async param| K1 MM2S -->|stream| PLIO_IN2 PLIO_IN2 --> K2 --> K3 --> PLIO_OUT2 PLIO_OUT2 -->|stream| S2MM_2 S2MM_1 --> DDR S2MM_2 --> DDR HOST0 -.->|xrt::bo| DDR HOST1 -.->|xrt::bo| DDR HOST2 -.->|xrt::bo| DDR style PR0 fill:#e1f5fe style PR1 fill:#fff3e0 style PR2 fill:#f3e5f5

分区配置详解

每个分区通过 aie.cfg 文件声明其资源边界:

# pr0_gmio/aie.cfg - GMIO 分区
[aie]
enable-partition=6:1:pr0   # 起始列:列数:分区名
                           # 第6列开始,占用1列

# pr1_rtp/aie.cfg - RTP 分区  
[aie]
enable-partition=8:1:pr1   # 第8列开始,占用1列

# pr2_perf/aie.cfg - 性能分区
[aie]  
enable-partition=10:2:pr2  # 第10列开始,占用2列

这种声明式的分区定义让编译器知道:

  1. 资源隔离:该分区的内核只能映射到指定的列范围内
  2. 命名空间隔离:端口名称自动加上前缀(如 pr1_Dataout0
  3. 独立仿真:可以使用 aiesimulator 单独验证每个分区

系统集成连接矩阵

system.cfg 定义了跨分区的数据流:

[connectivity]
# 实例化声明:内核类型:数量:实例名列表
nk=s2mm:2:s2mm_1,s2mm_2
nk=mm2s:1:mm2s_1
nk=datagen:1:datagen

# 流连接:源端口 -> 目标端口
stream_connect=ai_engine_0.pr1_Dataout0:s2mm_1.s   # pr1输出到s2mm_1
stream_connect=datagen.out:ai_engine_0.pr1_Datain0 # datagen输入到pr1
stream_connect=ai_engine_0.pr2_Dataout0:s2mm_2.s   # pr2输出到s2mm_2
stream_connect=mm2s_1.s:ai_engine_0.pr2_Datain0    # mm2s输入到pr2

关键洞察ai_engine_0 是 V++ 链接时创建的虚拟容器,它将多个 libadf.a 合并为一个统一的 AI Engine 子系统。端口命名遵循 {partition}_{port} 的约定。


各分区内部实现

有关各分区和 PL 内核的详细实现分析,请参考以下子模块文档:

pr0_gmio: GMIO 基础分区

这是最简单的分区,演示基本的 GMIO(Global Memory I/O)操作:

// pr0_gmio/aie/graph.h
class mygraph: public adf::graph {
    adf::kernel k_m;
public:
    adf::output_gmio gmioOut;  // 输出到全局内存
    adf::input_gmio gmioIn;    // 从全局内存输入

    mygraph(){
        // 创建加权求和内核(类似 FIR 滤波器)
        k_m = adf::kernel::create(vectorized_weighted_sum_with_margin);
        
        // 配置 GMIO 端口:64字节突发,1000ns 超时
        gmioOut = adf::output_gmio::create("gmioOut", 64, 1000);
        gmioIn = adf::input_gmio::create("gmioIn", 64, 1000);

        // 连接数据流
        adf::connect<>(gmioIn.out[0], k_m.in[0]);
        adf::connect<>(k_m.out[0], gmioOut.in[0]);
        
        adf::source(k_m) = "weighted_sum.cc";
        adf::runtime<adf::ratio>(k_m) = 0.9;  // 90% AIE 周期利用率
    }
};

内核实现亮点weighted_sum.cc):

void vectorized_weighted_sum_with_margin(
    input_buffer<int32, extents<256>, margin<8>> & restrict in,
    output_buffer<int32, extents<256>> & restrict out)
{
    // 使用 AIE API 进行向量化计算
    auto inIter = aie::begin_vector<8>(in);
    auto outIter = aie::begin_vector<8>(out);
    aie::vector<int32,8> coeffs = aie::load_v<8>(weights);  // {1,2,3,4,5,6,7,8}

    // 滑动窗口乘法累加(sliding_mul)
    // 每次处理16个元素,利用数据复用减少内存访问
    for(unsigned i=0; i<256/16; i++)
        chess_prepare_for_pipelining
        chess_loop_range(4,32)
    {
        data.insert(1, *inIter++);
        auto acc = aie::sliding_mul<4,8>(coeffs, 0, data, 1);
        // ... 展开循环以最大化吞吐量
    }
}

设计要点

  • margin<8> 声明了 8 个元素的边缘填充,支持 FIR 风格的历史数据访问
  • chess_prepare_for_pipelining 指导编译器进行软件流水线优化
  • restrict 关键字告诉编译器指针不别名,允许激进优化

pr1_rtp: 运行时参数分区

此分区引入 RTP(Run-Time Parameter) 机制,允许主机在图运行期间动态更新参数:

// pr1_rtp/aie/graph.h
class rtpgraph: public adf::graph {
    adf::kernel k;
public:
    adf::output_plio out;
    adf::input_plio in;
    adf::port<adf::direction::in> value;  // RTP 端口声明

    rtpgraph() {
        k = adf::kernel::create(vect_add<256>);
        out = adf::output_plio::create("Dataout0", adf::plio_32_bits, "data/output0.txt");
        in = adf::input_plio::create("Datain0", adf::plio_32_bits, "data/input0.csv");
        
        // 关键:异步连接 RTP 到内核的第二个输入
        adf::connect<adf::parameter>(value, adf::async(k.in[1]));
        
        adf::connect<>(k.out[0], out.in[0]);
        adf::connect<>(in.out[0], k.in[0]);
    }
};

主机端 RTP 更新

// pr1_rtp/sw/host.cpp
auto ghdl = xrt::graph(device, uuid, "pr1_gr");
ghdl.run(ITERATION);
ghdl.update("pr1_gr.k.in[1]", 10);  // 动态更新加数值为10
ghdl.end();

应用场景:系数更新、阈值调整、模式切换等需要运行时灵活性的场景。

pr2_perf: 高性能流水线分区

此分区展示多内核流水线和显式 FIFO 深度配置:

// pr2_perf/aie/graph.h
class adaptive_graph : public graph {
    kernel k[2];  // 两个级联的内核
public:
    input_plio in;
    output_plio dataout;

    adaptive_graph() {
        k[0] = kernel::create(aie_dest1);  // 数据分发
        k[1] = kernel::create(aie_dest2);  // 数据合并
        
        in = input_plio::create("Datain0", plio_128_bits, "data/input.txt");
        dataout = output_plio::create("Dataout0", plio_128_bits, "data/output.txt");

        runtime<ratio>(k[0]) = 0.8;  // 80% 利用率
        runtime<ratio>(k[1]) = 0.8;

        // 显式命名网络以便配置 FIFO 深度
        connect<> net0(in.out[0], k[0].in[0]);
        connect<> net1(k[0].out[0], k[1].in[0]);
        connect<> net2(k[0].out[1], k[1].in[1]);  // 双输出到双输入
        connect<> net3(k[1].out[0], dataout.in[0]);
        
        fifo_depth(net1) = 32;  // 显式设置中间缓冲区深度
    }
};

内核功能

  • aie_dest1: 将输入数据复制到两个输出(广播/分叉)
  • aie_dest2: 将两个输入相加后输出(归约/合并)
  • 整体效果:output = input + input = 2 * input

PL 内核:数据搬运工

PL(Programmable Logic)内核作为 AI Engine 与外部世界的桥梁:

mm2s: Memory-to-Stream

void mm2s(ap_uint<128>* mem, hls::stream<ap_axiu<128, 0, 0, 0>>& s, int size) {
    #pragma HLS INTERFACE m_axi port=mem offset=slave bundle=gmem
    #pragma HLS interface axis port=s
    #pragma HLS INTERFACE s_axilite port=mem bundle=control
    #pragma HLS INTERFACE s_axilite port=size bundle=control

    for(int i = 0; i < size; i++) {
        #pragma HLS PIPELINE II=1
        ap_axiu<128, 0, 0, 0> x;
        x.data = mem[i];
        x.keep = -1;  // 所有字节有效
        x.last = 0;   // 非包尾
        s.write(x);
    }
}

接口分析

  • m_axi: 连接到 DDR,支持突发传输
  • axis: AXI4-Stream 接口,连接到 AI Engine
  • s_axilite: 控制寄存器接口,用于传递 size 参数

s2mm: Stream-to-Memory

void s2mm(ap_int<32>* mem, hls::stream<ap_axis<32, 0, 0, 0>>& s, int size) {
    #pragma HLS INTERFACE m_axi port=mem offset=slave bundle=gmem
    #pragma HLS interface axis port=s
    // ... 控制接口同上

    for(int i = 0; i < size; i++) {
        #pragma HLS PIPELINE II=1
        ap_axis<32, 0, 0, 0> x = s.read();
        mem[i] = x.data;
    }
}

datagen: 测试数据生成器

void datagen(hls::stream<ap_axis<32,0,0,0>>& out, int size, int init_val=0) {
    #pragma HLS INTERFACE axis port=out
    #pragma HLS INTERFACE s_axilite port=return bundle=control
    #pragma HLS INTERFACE ap_ctrl_hs port=return
    // ...
    for(int i=0; i<size; i++){
        ap_axis<32,0,0,0> tmp;
        tmp.data = i + init_val;  // 生成递增序列
        tmp.keep = -1;
        tmp.last = 0;
        out.write(tmp);
    }
}

构建流程:从源码到比特流

flowchart LR subgraph Compile["1. 独立编译阶段"] A[pr0/aie/graph.cpp] -->|aiecompiler| A1[pr0/libadf.a] B[pr1/aie/graph.cpp] -->|aiecompiler| B1[pr1/libadf.a] C[pr2/aie/graph.cpp] -->|aiecompiler| C1[pr2/libadf.a] D[pl_kernels/*.cpp] -->|vitis_hls| D1[*.xo] end subgraph Link["2. 链接阶段"] A1 & B1 & C1 & D1 -->|v++ -l| E[aie_base_graph_hw.xsa] end subgraph Package["3. 打包阶段"] E -->|v++ -p| F[sd_card.img] H0[pr0/sw/host.cpp] -->|aarch64-g++| H0E[host0.exe] H1[pr1/sw/host.cpp] -->|aarch64-g++| H1E[host1.exe] H2[pr2/sw/host.cpp] -->|aarch64-g++| H2E[host2.exe] HA[sw/host.cpp] -->|aarch64-g++| HE[host_all.exe] H0E & H1E & H2E & HE --> F end style Compile fill:#e8f5e9 style Link fill:#fff3e0 style Package fill:#e3f2fd

Makefile 关键规则

# 1. 编译所有分区的 libadf.a
LIBADF = pr0_gmio/libadf.a pr1_rtp/libadf.a pr2_perf/libadf.a
${LIBADF}: 
    make -C pr0_gmio aie
    make -C pr1_rtp aie
    make -C pr2_perf aie

# 2. 链接生成 XSA(包含所有分区和 PL 内核)
\({XSA}: \){LIBADF} \({VPP_SPEC} \){XOS}
    \({VCC} -g -l --platform \){PLATFORM} \({XOS} \){LIBADF} \
           -t \({TARGET} \){VPP_FLAGS} -o $@

# 3. 打包 SD 卡镜像
package_\({TARGET}: \){LIBADF} \({XSA} \){HOST_EXE} 
    \({VCC} -p -s -t \){TARGET} -f ${PLATFORM} \
        --package.rootfs ${ROOTFS} \
        --package.kernel_image ${IMAGE} \
        --package.boot_mode=sd \
        --package.defer_aie_run \
        --package.sd_file ${HOST_EXE0} \
        --package.sd_file ${HOST_EXE1} \
        --package.sd_file ${HOST_EXE2} \
        --package.sd_file ${HOST_ALL} \
        \({XSA} \){LIBADF}

设计权衡与决策分析

1. 分区粒度选择

方案 优势 劣势 本模块选择
单一大图 全局优化机会多,资源利用率高 编译慢,协作难
细粒度分区(每内核一分区) 最大灵活性 连接开销大,时序难收敛
中等粒度分区(功能相关内核一组) 平衡开发与性能 需要预先规划

决策依据:三个分区分别对应不同的开发场景(GMIO 基础、RTP 高级特性、性能优化),既展示了多样性,又保持了合理的复杂度。

2. 通信机制选择

机制 适用场景 延迟 带宽
GMIO 大数据块 DDR 访问 较高 高(突发传输)
PLIO (32-bit) 控制流、低速数据
PLIO (128-bit) 高速流处理 很高

本模块应用

  • pr0 使用 GMIO:展示基本的 DDR 交互
  • pr1 使用 32-bit PLIO:匹配 RTP 的控制流特性
  • pr2 使用 128-bit PLIO:最大化吞吐量

3. 同步 vs 异步 RTP

// 本模块使用的异步 RTP
adf::connect<adf::parameter>(value, adf::async(k.in[1]));

异步 RTP 允许主机随时更新参数,无需等待图迭代边界。代价是:

  • 需要额外的握手逻辑
  • 参数更新时机不确定,可能影响确定性调试

4. 主机程序组织

提供多个独立主机程序而非单一程序:

./host0.exe a.xclbin  # 只运行 pr0
./host1.exe a.xclbin  # 只运行 pr1
./host2.exe a.xclbin  # 只运行 pr2
./host_all.exe a.xclbin  # 运行所有分区

优势

  • 便于独立验证每个分区
  • 清晰展示分区间的隔离性
  • 模拟真实的多团队协作场景

限制

  • 非 DFX 平台不支持图重新加载,必须重启板卡
  • 多个主机程序不能同时运行(共享同一硬件)

新贡献者注意事项

🔴 关键约束

  1. 分区列不能重叠

    # 错误示例!
    enable-partition=6:2:pr0   # 占用 col 6-7
    enable-partition=7:1:pr1   # 占用 col 7 → 冲突!
    
  2. 端口命名必须加前缀

    # graph.h 中定义的端口名
    out = adf::output_plio::create("Dataout0", ...);
    
    # system.cfg 中引用时必须加分区前缀
    stream_connect=ai_engine_0.pr1_Dataout0:s2mm_1.s
    
  3. 主机代码中图名也要加前缀

    // 错误
    auto ghdl = xrt::graph(device, uuid, "gr");
    
    // 正确
    auto ghdl = xrt::graph(device, uuid, "pr0_gr");
    

🟡 常见陷阱

问题 症状 解决方案
忘记设置环境变量 PLATFORM_REPO_PATHS 未定义错误 运行 environment-setup-cortexa72-cortexa53-xilinx-linux
分区列越界 编译报错或布局失败 检查平台规格,VCK190 有 50 列 AIE
流连接不匹配 链接错误或死锁 确认位宽一致(32-bit vs 128-bit)
主机程序崩溃 XRT 异常 确认使用非缓存缓冲区 xrt::bo::flags::normal

🟢 扩展建议

如需添加第四个分区:

  1. 创建 pr3_new/aie/ 目录结构
  2. 编写 graph.h, graph.cpp, 内核文件
  3. 创建 aie.cfg,选择合适的列(如 12:2:pr3
  4. 在顶层 Makefile 中添加 pr3_new/libadf.a
  5. system.cfg 中添加新的 stream_connect
  6. (可选)创建 pr3_new/sw/host.cpp

与其他模块的关系

上游依赖

本模块依赖于以下基础设施:

下游应用

本模块的技术被以下复杂系统采用:


总结

Independent Graphs Composition 模块展示了现代异构计算系统设计的核心理念:分而治之,合而用之。通过将 AI Engine 阵列划分为逻辑上独立的分区,多个团队可以并行工作,各自专注于自己的算法优化和验证,最终通过标准化的接口和工具链集成为完整的系统。

掌握这一模块的关键在于理解:

  1. 分区是编译时概念,最终仍运行在统一的硬件上
  2. 命名空间前缀是连接独立编译单元的纽带
  3. 流连接是分区间通信的唯一方式,需要精心设计
  4. 主机程序可以灵活选择控制粒度(单分区 vs 全系统)

这一设计模式不仅适用于 AI Engine,也为未来的大规模 FPGA/ACAP 设计提供了可扩展的方法论框架。

On this page