🏠

hls_x10_multi_instance_system_config 技术深度解析

概述:这个模块解决什么问题?

想象你正在设计一个工厂流水线。最初,你可能只有一条生产线(x1配置),但很快发现产能不足。于是你增加到5条并行生产线(x5配置),最后扩展到10条(x10配置)。hls_x10_multi_instance_system_config 就是这个"10条并行生产线"的配置蓝图——它定义了如何在AMD Versal器件上部署10个独立的2D FFT计算单元,每个单元配备专用的DMA数据搬运引擎。

这个模块的核心价值在于水平扩展性。在信号处理领域,2D FFT是计算密集型任务,单个实例的吞吐量往往无法满足实时需求。通过将10个fft_2d内核与10个dma_hls数据搬运器配对部署,系统能够实现近线性的性能提升。这不是简单的复制粘贴——每个实例都有独立的AXI-Stream端口连接,形成完全隔离的数据通路,避免了资源争用和带宽瓶颈。

架构思维模型:工厂流水线的类比

理解这个配置文件的最好的方式是将其视为一个工业园区的规划图

┌─────────────────────────────────────────────────────────────────────────────┐
│                         x10 工业园区(Versal Device)                        │
├─────────────────────────────────────────────────────────────────────────────┤
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                   │
│  │  dma_hls_0   │◄──►│  fft_2d_0    │◄──►│  dma_hls_0   │  ← 第0号车间      │
│  │  (原料入口)   │    │  (加工中心)   │    │  (成品出口)   │                   │
│  └──────────────┘    └──────────────┘    └──────────────┘                   │
│                                                                              │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                   │
│  │  dma_hls_1   │◄──►│  fft_2d_1    │◄──►│  dma_hls_1   │  ← 第1号车间      │
│  └──────────────┘    └──────────────┘    └──────────────┘                   │
│                                                                              │
│                              ...                                             │
│                                                                              │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                   │
│  │  dma_hls_9   │◄──►│  fft_2d_9    │◄──►│  dma_hls_9   │  ← 第9号车间      │
│  └──────────────┘    └──────────────┘    └──────────────┘                   │
└─────────────────────────────────────────────────────────────────────────────┘

每个"车间"包含三个关键组件:

  1. DMA入口 (mm2s0):从外部存储读取原始数据,转换为AXI-Stream格式
  2. 2D FFT计算核心 (fft_2d):执行行方向FFT → 转置 → 列方向FFT的完整变换
  3. DMA出口 (s2mm1):接收处理结果,验证正确性

中间的dmaHls_rowsToCols则像一个智能中转站,它接收行FFT的输出,进行数据重排(模拟转置操作),然后送入列FFT阶段。

核心配置文件解析

1. 内核实例化声明

[connectivity]
nk=fft_2d:10:fft_2d_0.fft_2d_1.fft_2d_2.fft_2d_3.fft_2d_4.fft_2d_5.fft_2d_6.fft_2d_7.fft_2d_8.fft_2d_9
nk=dma_hls:10:dma_hls_0.dma_hls_1.dma_hls_2.dma_hls_3.dma_hls_4.dma_hls_5.dma_hls_6.dma_hls_7.dma_hls_8.dma_hls_9

设计意图解读

  • nk=<kernel>:<count>:<instance_names> 是Vitis Linker的语法
  • 这里的关键决策是命名约定——使用下划线+数字后缀(_0_9)而非其他命名方案
  • 这种命名方式使得批量生成stream_connect语句成为可能(通过脚本或模板)

2. AXI-Stream连接拓扑

以第0号实例为例:

# 行FFT数据通路(Row-wise FFT Path)
stream_connect=dma_hls_0.strmOut_to_rowiseFFT:fft_2d_0.strmFFTrows_inp
stream_connect=fft_2d_0.strmFFTrows_out:dma_hls_0.strmInp_from_rowiseFFT

# 列FFT数据通路(Column-wise FFT Path)
stream_connect=dma_hls_0.strmOut_to_colwiseFFT:fft_2d_0.strmFFTcols_inp
stream_connect=fft_2d_0.strmFFTcols_out:dma_hls_0.strmInp_from_colwiseFFT

数据流详解

dma_hls_0                    fft_2d_0                     dma_hls_0
┌─────────┐                 ┌─────────────┐              ┌─────────┐
│ mm2s0   │──strmOut_to_───►│             │              │         │
│ (输入)   │   rowiseFFT     │  fft_rows() │──strmFFTrows_─►│dmaHls_  │
└─────────┘                 │             │   out          │rowsToCols│
                            └─────────────┘                │  (中转)  │
                                                           └────┬────┘
                                                                │
                           ┌─────────────┐                      │
                           │             │◄──strmOut_to_─────────┘
                           │  fft_cols() │   colwiseFFT
                           │             │
                           └──────┬──────┘
                                  │
                           strmFFTcols_out
                                  │
                                  ▼
                            ┌─────────┐
                            │ s2mm1   │
                            │ (输出)   │
                            └─────────┘

关键观察

  • 每个fft_2d实例有4个AXI-Stream端口:行输入、行输出、列输入、列输出
  • 这些端口全部连接到同一个dma_hls实例的不同逻辑通道
  • 这种设计选择意味着每个FFT实例独占一个DMA控制器,避免了多实例间的资源竞争

3. 高级编译选项

[advanced]
# Disable Profiling in hw_emu so that it is faster...
param=hw_emu.enableProfiling=false

# Export the xsa of the design..
param=compiler.addOutputTypes=hw_export

工程权衡分析

  • hw_emu.enableProfiling=false:在硬件仿真模式下禁用性能分析,换取更快的仿真速度

    • 为什么这是合理的:x10配置的仿真已经相当耗时,profiling会进一步拖慢迭代周期
    • 风险:如果在调试阶段需要查看信号波形,需要手动重新启用
  • compiler.addOutputTypes=hw_export:生成XSA(Xilinx Support Archive)文件

    • 用途:XSA包含了完整的硬件平台描述,可用于后续的PetLinux集成或Vitis应用开发

数据流深度追踪

让我们跟随一个数据样本的完整生命周期:

Phase 1: 数据注入(Data Injection)

发生在dma_hls.cppmm2s0函数:

void mm2s0(
   hls::stream<ap_axiu<128, 0, 0, 0>> &strmOut_to_rowiseFFT,
   ap_uint<25> matSz, ap_int<16> iterCnt
)
{
   MM2S0_ITER:while(iterCnt--) {
      MM2S0:for(ap_uint<25> i = 0; i < matSz; ++i) {
         #pragma HLS PIPELINE II=1
         // ...
         ap_axiu<128, 0, 0, 0> fftRow_inp;
         if(i == 0) {
            fftRow_inp.data = INP_DATA;  // 脉冲输入
         } else {
            fftRow_inp.data = 0;
         }
         strmOut_to_rowiseFFT.write(fftRow_inp);
      }
   }
}

关键实现细节

  • 使用ap_axiu<128, 0, 0, 0>表示128位数据宽度,无用户/ID/保持信号
  • PIPELINE II=1确保每个时钟周期输出一个数据包
  • 测试模式使用脉冲输入(第一个元素为1,其余为0),便于验证FFT的正确性

Phase 2: 行方向FFT(Row-wise FFT)

发生在fft_2d.cppfft_rows函数:

void fft_rows(
      hls::stream<ap_axiu<128, 0, 0, 0>> &strm_inp,
      hls::stream<ap_axiu<128, 0, 0, 0>> &strm_out
)
{
   LOOP_FFT_ROWS:for(int i = 0; i < MAT_ROWS; ++i) {
      #pragma HLS DATAFLOW
      
      cmpxDataIn rows_in[MAT_COLS];
      #pragma HLS STREAM variable=rows_in depth=1024
      
      cmpxDataOut rows_out[MAT_COLS];
      #pragma HLS STREAM variable=rows_out depth=1024
      
      readIn_row(strm_inp, rows_in);
      fftRow(directionStub, rows_in, rows_out, &ovfloStub);
      writeOut_row(strm_out, rows_out);
   }
}

HLS优化策略解读

  • DATAFLOW指令让readIn_rowfftRowwriteOut_row形成三级流水线
  • STREAM depth=1024创建了足够深的FIFO来缓冲整行数据(假设MAT_COLS ≤ 1024)
  • 数据类型根据FFT_2D_DT宏在cint16(16位复整数)和cfloat(32位浮点)间切换

Phase 3: 行列转换中转(Row-to-Column Transpose)

这是整个设计中最巧妙的部分。注意:没有显式的转置操作

void dmaHls_rowsToCols(
      hls::stream<ap_axiu<128, 0, 0, 0>> &strmInp_from_rowiseFFT,
      hls::stream<ap_axiu<128, 0, 0, 0>> &strmOut_to_colwiseFFT,
      // ...
)
{
   // Stage 0: 验证行FFT输出
   S2MM0:for(ap_uint<25> i = 0; i < matSz; ++i) {
      ap_axiu<128, 0, 0, 0> fftRow_out = strmInp_from_rowiseFFT.read();
      // 验证逻辑...
   }
   
   // Stage 1: 生成列FFT输入(以列优先顺序)
   MM2S1:for(ap_uint<25> i = 0, idx = 0; i < matSz; ++i) {
      ap_axiu<128, 0, 0, 0> fftCol_inp;
      if(i == idx) {
         fftCol_inp.data = INP_DATA;
         idx += rows;  // 关键:按列步进
      }
      strmOut_to_colwiseFFT.write(fftCol_inp);
   }
}

设计洞察

  • 这个模块实际上是一个自包含的测试生成器,不是真正的转置器
  • 它丢弃行FFT的输出,然后重新生成符合列FFT期望模式的输入
  • idx += rows的逻辑模拟了列优先访问模式
  • 为什么这样做:为了测量纯FFT计算的峰值性能,避免DDR带宽成为瓶颈

Phase 4: 列方向FFT与验证(Column-wise FFT & Verification)

void s2mm1(
      hls::stream<ap_axiu<128, 0, 0, 0>> &strmInp_from_colwiseFFT,
      ap_uint<25> matSz, ap_uint<25> &stg1_errCnt, 
      ap_uint<128> goldenVal, ap_int<16> iterCnt
)
{
   S2MM1:for(ap_uint<25> i = 0; i < matSz; ++i) {
      ap_axiu<128, 0, 0, 0> fftCol_out = strmInp_from_colwiseFFT.read();
      if(fftCol_out.data != goldenVal) {
         ++stg1_errCnt;
      }
   }
}

自检机制

  • 期望所有输出都等于goldenVal(全1模式)
  • 错误计数器累加任何不匹配
  • 最终返回给host用于验证测试通过/失败

设计决策与权衡

决策1:独立实例 vs 共享资源

方案 优点 缺点
当前:10个独立实例 无资源争用,确定性时序,易于调试 面积开销大,功耗高
替代:共享DMA引擎 节省PL资源 需要仲裁逻辑,可能引入延迟抖动
替代:时分复用 更少的硬件实例 吞吐量下降,控制复杂

选择理由:这是一个性能优先的设计,目标是最大化2D FFT的吞吐量。Versal器件的PL资源丰富,可以容纳10个完整的FFT+DMA流水线。

决策2:片上自检 vs Host端验证

当前设计在PL内部完成数据生成和结果验证,不依赖外部DDR。这带来几个后果:

  • 优势

    • 消除了NoC/DDR带宽瓶颈,测量的是纯计算性能
    • 减少了host代码复杂度
    • 测试结果确定性强(黄金数据已知)
  • 局限

    • 无法处理真实的外部数据源
    • 不适合作为通用2D FFT加速器(仅适合基准测试)

如果你需要处理真实数据,应该参考同目录下的x1单实例配置,它通常有更通用的DDR接口设计。

决策3:cint16 vs cfloat 数据类型

通过FFT_2D_DT宏在编译时选择:

#if FFT_2D_DT == 0 // cint16 datatype
   typedef ap_fixed<16, 1> data_in_t;
   // 每128-bit AXI传输4个样本
#else // cfloat datatype
   typedef float data_in_t;
   // 每128-bit AXI传输2个样本
#endif

量化影响

  • cint16:更高的样本吞吐(4样本/周期),但需要缩放管理防止溢出
  • cfloat:动态范围大,无需缩放,但吞吐减半(2样本/周期)

配置文件中未指定数据类型,这意味着它由构建系统的Makefile或Tcl脚本传入的宏定义决定。

新贡献者注意事项

1. 实例编号的一致性陷阱

配置文件中使用了硬编码的_0_9后缀。如果修改实例数量,必须同步更新

  • nk=行的实例名列表
  • 所有stream_connect=行的端口名
  • Host代码中的内核打开调用(xrt::kernel构造)

建议:使用脚本生成配置文件,避免手动编辑出错。

2. Stream Depth的资源影响

fft_2d.cpp中定义的STREAM depth(如depth=1024)直接影响FPGA BRAM用量:

#pragma HLS STREAM variable=rows_in depth=1024
// cint16 = 32 bits per sample × 1024 = 4KB per buffer
// 每个fft_2d实例有多个此类缓冲区

在x10配置下,这些小buffer会累积成显著的BRAM消耗。如果遇到资源溢出,考虑:

  • 减小STREAM depth(可能影响性能)
  • 使用URAM替代BRAM(需要在HLS中指定storage=uram

3. 硬件仿真 vs 实际硬件的行为差异

配置中禁用了硬件仿真的profiling(hw_emu.enableProfiling=false)。这意味着:

  • hw_emu模式下运行更快,但看不到详细的性能报告
  • 如果需要调试性能问题,临时注释掉这一行
  • 实际硬件运行(hw模式)不受此设置影响

4. 与AIE版本的对比

本模块属于HLS实现路径。同一教程还包含AIE版本,两者关键差异:

特性 HLS版本(本模块) AIE版本
计算单元 PL中的HLS内核 AI Engine阵列
数据移动 PL DMA + AXI-Stream AIE Shim DMA + 专用stream
适用场景 中等粒度并行 大规模数据并行
编程模型 C++ + HLS pragmas Dataflow graph + kernel C++

5. 扩展至更多实例的路径

如果需要超过10个实例(例如x20):

  1. 检查PL资源:使用report_metrics.tcl查看当前利用率
  2. 考虑NoC带宽:更多实例意味着更高的AXI-Stream流量,确保NoC配置能支撑
  3. Host调度策略:当前设计假设host同时启动所有实例。实例增多时,可能需要分批启动以避免瞬时功耗峰值

相关模块导航

总结

hls_x10_multi_instance_system_config代表了空间并行扩展的极致——通过复制完整的计算流水线来线性提升吞吐量。它不是最灵活的架构(无法动态调整实例数),也不是最面积高效的(实例间零资源共享),但它提供了可预测的性能极简的编程模型:host只需同时启动10个内核,等待它们全部完成,然后收集聚合的误差计数。

对于刚加入团队的工程师,建议从这个配置文件出发,逆向理解Vitis的链接流程:.cfg文件如何被v++链接器解析,如何生成最终的.xclbin。这将为你后续开发更复杂的异构系统打下坚实基础。

On this page