HLS Synthesis and Optimized Build Flows
概述
本模块是旅行商问题(TSP)硬件加速设计教程的核心构建层,负责将C++算法描述通过Vitis HLS工具链转换为可综合的RTL硬件实现。它包含两个互补的构建流程:基础HLS合成流程与优化版HLS合成流程,分别对应不同的性能与资源权衡策略。
可以把这两个流程想象成同一条生产线的"标准版"和"高性能版"——它们处理相同的原材料(C++源代码),但优化版通过并行化改造和存储器分区技术,在消耗更多芯片面积的前提下实现了数倍的速度提升。
架构设计
整体结构
基础构建流程] --> C[共享内核逻辑] B[hls_opt.tcl
优化构建流程] --> C C --> D[tsp.h
公共头文件] C --> E[tsp.cpp
基础实现] C --> F[tsp_opt.cpp
优化实现] end subgraph "测试验证层" G[tsp_TB.cpp
统一测试平台] end C -.-> G
核心组件职责
| 组件 | 类型 | 职责 |
|---|---|---|
hls.tcl |
TCL脚本 | 基础版HLS项目配置与构建编排 |
hls_opt.tcl |
TCL脚本 | 优化版HLS项目配置与构建编排 |
tsp.h |
C++头文件 | 公共类型定义、常量配置、函数接口声明 |
tsp.cpp |
C++源文件 | 串行计算的基础TSP内核实现 |
tsp_opt.cpp |
C++源文件 | 4路并行的优化TSP内核实现 |
tsp_TB.cpp |
C++测试文件 | 统一的C/RTL协同仿真测试平台 |
双版本设计哲学
为什么需要两个版本?
这个设计体现了硬件开发中的经典权衡:面积(Area)vs 吞吐量(Throughput)。
基础版 (tsp.cpp) 采用单路串行计算:
- 优势:资源占用少,时序收敛容易,适合资源受限的场景
- 劣势:每个时钟周期只能评估一个候选路径
优化版 (tsp_opt.cpp) 采用4路空间并行:
- 优势:理论加速比接近4倍(实际受限于存储器端口和最终归约步骤)
- 劣势:BRAM用量增加4倍,布线复杂度上升,功耗增加
这种"双轨制"让开发者能够根据目标FPGA的剩余资源和时序裕量灵活选择实现方案。
关键差异对比
| 特性 | 基础版 (tsp.cpp) | 优化版 (tsp_opt.cpp) |
|---|---|---|
| 距离矩阵存储 | 单个 distances[N][N] |
4个副本 distances_0~3[N][N] |
| 主循环步进 | i_ += 1 |
i_ += 4 |
| 候选结果寄存器 | 单个 candidate |
4个 candidate0~3 |
| 最终归约 | 无(直接输出) | std::min({c0,c1,c2,c3}) |
| 存储器类型 | ram_1wnr (1写多读) |
4x ram_1wnr |
数据流分析
端到端数据路径
streamDistances] --> B[距离矩阵加载
loop_distances] B --> C{并行计算阵列} C --> D[候选结果归约] D --> E[标量输出
shortestDistance] subgraph "优化版特有" C --> C1[compute(i+0)] C --> C2[compute(i+1)] C --> C3[compute(i+2)] C --> C4[compute(i+3)] C1 --> D C2 --> D C3 --> D C4 --> D end
详细数据流转
- 输入阶段:距离矩阵通过AXI-Stream接口流入,按行优先顺序填充本地存储器
- 计算阶段:主循环以II=1(Initiation Interval)的速率发射计算任务
- 基础版:每次迭代发射1个
compute()调用 - 优化版:每次迭代并行发射4个
compute()调用,各自访问独立的距离矩阵副本
- 基础版:每次迭代发射1个
- 归约阶段:优化版需要将4路部分结果合并为全局最小值
- 输出阶段:最终结果通过引用参数返回
HLS编译流程详解
TCL脚本执行序列
两个TCL脚本遵循相同的执行模式:
# 1. 项目初始化
open_project -reset proj # 创建/重置项目
set_top tsp # 指定顶层函数
add_files ./code/tsp.cpp # 添加设计源文件
add_files -tb ./code/tsp_TB.cpp # 添加测试平台
# 2. 解决方案配置
open_solution "solution1" -flow_target vivado
set_part {xcvu9p-flga2104-2-i} # 目标器件:Virtex UltraScale+
create_clock -period 3.0 # 333MHz目标频率
set_clock_uncertainty 0.5 # 500ps时钟不确定性裕量
config_export -format ip_catalog # 导出为IP Catalog格式
# 3. 综合与验证流程
csim_design # C语言功能仿真
csynth_design # HLS高层次综合
cosim_design # C/RTL协同仿真
export_design -flow impl ... # 导出实现级网表
关键时序约束
- 时钟周期:3.0ns(约333MHz)
- 时钟不确定性:0.5ns(16.7%的裕量,用于布局布线后的时序收敛)
- 目标器件:
xcvu9p-flga2104-2-i—— Virtex UltraScale+ VU9P,属于高端数据中心FPGA
核心算法实现
排列生成算法(Lehmer Code)
compute()函数使用阶乘数系(Factorial Number System)将循环索引映射为城市排列:
// 步骤1:将索引转换为阶乘进制表示
for (int k = 0; k < N; ++k) {
perm[k] = i / factorial(N - 1 - k); // 商作为当前位
i = i % factorial(N - 1 - k); // 余数继续分解
}
// 步骤2:转换为实际排列(Lehmer编码解码)
for (char k = N - 1; k > 0; --k)
for (char j = k - 1; j >= 0; --j)
perm[k] += (perm[j] <= perm[k]); // 调整重复值
这种算法的优势在于无需递归或堆栈,完全适合硬件流水线实现。
距离计算与比较
unsigned int getDistance(const T perm[N], const uint16_t distances[N][N])
{
unsigned int ret = 0;
for(int i = 0; i < N-1; ++i)
ret += distances[perm[i]][perm[i+1]]; // 累加相邻城市距离
return ret;
}
硬件接口设计
AXI-Stream数据输入
#pragma HLS INTERFACE axis port=streamDistances
- 协议:AXI4-Stream
- 数据宽度:16位(
uint16_t) - 传输内容:展平的距离矩阵(\(N \times N\) 个元素)
存储器接口配置
#pragma HLS BIND_STORAGE variable=distances type=ram_1wnr
ram_1wnr:单口RAM,1个写端口,N个读端口- 这是支持II=1流水线的关键——每个周期需要同时读取多个距离值
标量输出
void tsp(..., unsigned int& shortestDistance)
- 通过引用传递,HLS会自动推断为
ap_vld或类似接口 - 在函数结束时一次性写入最终结果
设计权衡与决策分析
1. 存储器复制 vs 多端口存储器
选择的方案:在优化版中复制4份距离矩阵
替代方案:使用真正的多端口BRAM或URAM
权衡分析:
- 复制方案的BRAM用量是线性增长(4x),但布线简单,时序易收敛
- 多端口方案可能节省部分存储资源,但会增加布线拥塞和时序风险
- 对于小规模N(默认N=5),4x复制的资源开销完全可以接受
2. 循环展开因子选择
选择的方案:4路并行(i_ += 4)
为何不是8路或16路?
- 收益递减:更多并行度需要更多存储器副本,但最终的4输入
std::min会成为瓶颈 - 资源平衡:4路并行在VU9P器件上达到合理的资源利用率而不至于过度消耗
- 代码可读性:手动展开4路仍在可维护范围内
3. 归约树的位置
选择的方案:在软件循环结束后用std::min({...})进行最终归约
替代方案:在每个迭代内部进行部分归约
权衡分析:
- 当前方案减少了每轮迭代的逻辑深度,有利于维持II=1
- 4个32位整数的比较延迟可以忽略不计
- 如果并行度更高(如16路),可能需要考虑分层归约树
4. 定点数 vs 浮点数
选择的方案:输入使用归一化的uint16_t,内部累加使用unsigned int
权衡分析:
- 避免了浮点运算的高延迟和高资源消耗
- 16位精度足以表示归一化后的城市间距离
- 32位累加器防止了溢出(最大路径长度 \(N \times max(uint16_t)\))
新贡献者注意事项
修改N值的连锁反应
N(城市数量)在tsp.h中定义为编译时常量:
const int N = 5;
修改时必须同步检查:
- 测试平台中的预期结果数组
expected[]只有11个元素(N=1到11) - 排列总数为 \((N-1)!\),确保64位索引不会溢出
- 距离矩阵大小为 \(N^2\),影响AXI-Stream传输事务长度
HLS INLINE的使用限制
#pragma HLS INLINE
auto compute(...)
compute()被强制内联到主循环体中,这意味着:
- 无法单独对
compute()进行综合报告分析 - 任何对
compute()的修改都会直接影响主循环的时序 - 如果需要调试,可以临时移除INLINE pragma,但会影响整体QoR
存储器依赖与II约束
如果尝试进一步增加并行度,可能会遇到:
- 存储器端口竞争:多个
compute()实例同时读取同一距离矩阵 - 解决方案:继续增加矩阵副本数(distances_4, distances_5...)
- 代价:BRAM用量线性增长,布线难度增加
测试平台的硬编码限制
assert(N<12);
测试平台限制了最大城市数,原因包括:
- 预期结果数组的大小限制
- 仿真时间随\((N-1)!\)阶乘增长
- 浮点到定点转换的精度验证范围
时钟域交叉注意事项
虽然本模块是纯HLS内核,但在系统集成时:
- AXI-Stream接口会连接到系统的数据通路时钟域
- 标量输出可能连接到控制寄存器时钟域
- 确保
create_clock -period 3.0与系统级时钟规划一致
与其他模块的关系
本模块位于旅行商问题设计教程的构建层,向上支撑完整的系统设计:
- traveling_salesperson_hls_and_reference_flow:父模块,包含完整的教程上下文和CPU参考实现
- kernel_orchestration层:本模块生成的IP核将被集成到更大的系统中
- host_dispatch_variants:主机端代码通过XRT调用本模块生成的内核
总结
hls_synthesis_and_optimized_build_flows模块展示了从算法到硬件的标准转化路径,以及如何通过存储器复制和循环展开实现性能优化。理解这个模块的关键在于把握空间并行度与存储资源之间的线性关系,以及流水线II=1约束下的存储器访问模式设计。对于希望扩展此设计的开发者,建议首先通过HLS综合报告确认当前设计的资源利用率瓶颈,再决定是增加并行度还是优化数据通路。