Independent Graphs Composition(独立图组合)模块深度解析
一句话概括
本模块展示了如何在 AMD Versal AI Engine 上将一个大型系统拆分编译为多个独立的硬件分区,让每个团队可以独立开发、仿真和验证自己的子系统,最后通过 V++ 链接器将它们无缝集成为一个完整的可执行系统——就像把多个独立开发的乐高模块拼接成一个复杂的机械装置。
问题空间:为什么需要这个模块?
传统单体设计的痛点
在传统的 AI Engine 开发流程中,整个系统的所有计算图(graph)必须在一次编译中完成:
- 协作困难:多个团队同时修改同一个代码库,冲突频繁
- 编译时间爆炸:任何小改动都需要重新编译整个系统
- 验证瓶颈:无法单独验证某个子模块,必须等整个系统就绪
- 集成风险:问题只能在最后阶段发现,修复成本极高
独立分区方案的解决思路
想象一个城市规划项目:与其让所有建筑师挤在一个大房间里画一张巨型蓝图,不如把城市划分为若干个街区,每个团队负责一个街区的详细设计,最后由城市规划部门确保街区之间的道路和管线正确连接。
本模块正是这一思想在硬件设计中的实现:
- 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) │ └─────────────────────────┘ │
│ │ │
└─────────────────────────────┴────────────────────────────────┘
架构详解与数据流
整体架构图
控制 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列
这种声明式的分区定义让编译器知道:
- 资源隔离:该分区的内核只能映射到指定的列范围内
- 命名空间隔离:端口名称自动加上前缀(如
pr1_Dataout0) - 独立仿真:可以使用
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 接口、系统连接配置和 HLS 内核的深入解析
- datagen 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 Engines_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);
}
}
构建流程:从源码到比特流
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 平台不支持图重新加载,必须重启板卡
- 多个主机程序不能同时运行(共享同一硬件)
新贡献者注意事项
🔴 关键约束
-
分区列不能重叠
# 错误示例! enable-partition=6:2:pr0 # 占用 col 6-7 enable-partition=7:1:pr1 # 占用 col 7 → 冲突! -
端口命名必须加前缀
# graph.h 中定义的端口名 out = adf::output_plio::create("Dataout0", ...); # system.cfg 中引用时必须加分区前缀 stream_connect=ai_engine_0.pr1_Dataout0:s2mm_1.s -
主机代码中图名也要加前缀
// 错误 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 |
🟢 扩展建议
如需添加第四个分区:
- 创建
pr3_new/aie/目录结构 - 编写
graph.h,graph.cpp, 内核文件 - 创建
aie.cfg,选择合适的列(如12:2:pr3) - 在顶层
Makefile中添加pr3_new/libadf.a - 在
system.cfg中添加新的stream_connect - (可选)创建
pr3_new/sw/host.cpp
与其他模块的关系
上游依赖
本模块依赖于以下基础设施:
- versal_integration_data_movers: 提供基础的 DMA 数据搬运机制
- rtp_reconfiguration_flows: RTP 机制的详细教程
下游应用
本模块的技术被以下复杂系统采用:
- prime_factor_fft_pipeline_graphs: 大规模 FFT 的分区实现
- channelizer_ifft_and_tdm_fir_graphs: 多通道信号处理的分区策略
- n_body_packetized_pl_aie_connectivity: 大规模粒子仿真的分区通信
总结
Independent Graphs Composition 模块展示了现代异构计算系统设计的核心理念:分而治之,合而用之。通过将 AI Engine 阵列划分为逻辑上独立的分区,多个团队可以并行工作,各自专注于自己的算法优化和验证,最终通过标准化的接口和工具链集成为完整的系统。
掌握这一模块的关键在于理解:
- 分区是编译时概念,最终仍运行在统一的硬件上
- 命名空间前缀是连接独立编译单元的纽带
- 流连接是分区间通信的唯一方式,需要精心设计
- 主机程序可以灵活选择控制粒度(单分区 vs 全系统)
这一设计模式不仅适用于 AI Engine,也为未来的大规模 FPGA/ACAP 设计提供了可扩展的方法论框架。