4. 流水线与数据流:PIPELINE与DATAFLOW pragma

各位同学,今天我们来聊聊HLS加速里最核心的两个概念——流水线和数据流。说实话,我刚开始接触HLS时,总觉得这两个东西差不多,都是让数据“流起来”。但实际项目做多了才发现,它们完全是两码事,解决的问题也不一样。

4.1 循环展开与流水线:从串行到并行

先说说循环。在C/C++里写个for循环,默认情况下,HLS会把它综合成一个串行执行的硬件。什么意思呢?就是循环体里的操作,一次只做一个,做完一次再进下一次。这显然很慢。

那怎么加速?两个手段:循环展开和循环流水线。

4.1.1 循环展开(Loop Unrolling)

循环展开,说白了就是把循环体复制多份。比如一个循环要执行4次,你把它完全展开,就变成了4份独立的代码。硬件上,这4份操作可以同时执行。

看个例子:

// 原始代码
for (int i = 0; i < 4; i++) {
    a[i] = b[i] + c[i];
}

// 完全展开后
a[0] = b[0] + c[0];
a[1] = b[1] + c[1];
a[2] = b[2] + c[2];
a[3] = b[3] + c[3];

展开后,4个加法器可以同时工作。代价呢?硬件资源翻了4倍。我有个项目,为了追求极致性能,把一个大循环完全展开了,结果资源爆了,最后不得不改成部分展开。

关键点:循环展开是用面积换速度。展开因子越大,并行度越高,但资源消耗也越大。

4.1.2 循环流水线(Loop Pipelining)

循环流水线就不一样了。它不复制硬件,而是让循环的不同迭代在时间上重叠执行。就像工厂流水线,每个工位只做一件事,但所有工位同时都在干活。

用pragma来指定:

#pragma HLS PIPELINE II=1
for (int i = 0; i < N; i++) {
    a[i] = b[i] + c[i];
    d[i] = a[i] * e[i];
}

这里的II=1表示启动间隔(Initiation Interval)为1,意思是每个时钟周期都能启动一次新的循环迭代。理想情况下,整个循环的吞吐量就上去了。

嗯,这里要注意:II=1不是总能做到的。如果循环体里有数据依赖,比如当前迭代要用到上一次的结果,那II就得变大。我在项目中遇到过这种情况,当时为了把II从3压到1,折腾了好几天。

我的经验:优先用PIPELINE,因为它资源开销小。只有当PIPELINE无法满足性能要求时,才考虑展开。

4.2 DATAFLOW:函数级流水线

说完了循环,咱们聊聊函数。一个大的图像处理算法,通常拆成好几个函数,比如:灰度化 → 高斯滤波 → 边缘检测。默认情况下,这些函数是串行执行的——第一个函数处理完所有数据,第二个才开始。

这显然浪费了硬件能力。为什么不能像流水线一样,第一个函数处理完第一行数据,第二个函数就开始处理呢?

这就是DATAFLOW的作用。

void image_pipeline(hls::stream<uint8> &src, hls::stream<uint8> &dst) {
    #pragma HLS DATAFLOW

    hls::stream<uint8> gray, blur, edge;

    // 三个函数
    rgb2gray(src, gray);
    gaussian_blur(gray, blur);
    sobel_edge(blur, edge);

    // 输出
    dst = edge;
}

加上#pragma HLS DATAFLOW后,这三个函数就能同时工作。每个函数处理完一小块数据,就传给下一个。整个系统就像一个三级流水线。

我曾经踩过的坑:DATAFLOW要求函数之间通过hls::stream或FIFO传递数据。如果用普通数组或指针,HLS会报错,或者综合出来的性能很差。我第一次用的时候,直接传了个全局数组,结果DATAFLOW根本没生效。

4.3 PIPELINE vs DATAFLOW:到底用哪个?

很多初学者会搞混这两个概念。我画个图帮你理一理:

PIPELINE(循环级) 迭代1 迭代2 迭代3 时间→ 同一循环的不同迭代在时间上重叠 DATAFLOW(函数级) 函数A 函数B 函数C 不同函数同时处理不同数据块 PIPELINE vs DATAFLOW 对比 维度 PIPELINE DATAFLOW 作用范围 单个循环内部 函数/任务之间 并行粒度 循环迭代级 数据块级 典型应用 像素处理循环 多级图像滤波

简单总结一下:

  • PIPELINE:用在循环内部,让不同迭代重叠执行。适合计算密集型的循环体。
  • DATAFLOW:用在函数之间,让不同函数并行处理数据。适合多级流水线架构。

实际项目中,两者经常配合使用。比如,在DATAFLOW的每个函数内部,再用PIPELINE加速循环。我做过一个实时视频处理项目,就是这种组合拳,效果非常好。

4.4 实战建议与避坑指南

最后,分享几个我在项目中积累的经验:

  1. 先分析数据依赖。不管是PIPELINE还是DATAFLOW,数据依赖都是最大的敌人。先画出数据流图,找出依赖链,再决定优化策略。
  2. II值不是越小越好。II=1当然理想,但如果为了达到II=1而引入大量资源,反而得不偿失。我一般先设II=2或3,看看资源消耗,再决定要不要压。
  3. DATAFLOW的深度要适中。函数级流水线不是越深越好。太深的流水线,控制逻辑复杂,而且数据缓冲也会占用大量BRAM。3到5级是比较合理的范围。
  4. 仿真验证不能省。加了PIPELINE和DATAFLOW后,时序行为会变复杂。我习惯先用C仿真验证功能正确,再跑RTL协同仿真确认时序。

核心思想:流水线和数据流,本质上都是让硬件“忙起来”。不要让任何模块闲着,这就是加速的秘诀。

好了,这一章的内容就到这里。记住,理论懂了不算真懂,动手写过代码才算。建议你找个简单的图像处理算法,试试PIPELINE和DATAFLOW的效果,感受一下硬件加速的魅力。

可可精品资料库