3. 流水线基础语法:#pragma HLS PIPELINE、II(Initiation Interval)概念、流水线深度、自动流水线
各位同学,今天我们来聊聊HLS流水线的核心语法。说实话,流水线这个概念,很多初学者觉得不就是个pragma吗?加一行代码就完事了。但我在实际项目中踩过的坑告诉我——真正理解II、流水线深度这些概念,才能写出高性能的RTL。
3.1 #pragma HLS PIPELINE——流水线的开关
先看最基础的语法。在Vivado HLS中,启用流水线只需要在循环或函数前加上一行:
#pragma HLS PIPELINE
就这么简单?嗯,但背后的门道可不少。我个人习惯把PIPELINE理解为「流水线开关」,它告诉编译器:这个循环的每次迭代,可以重叠执行。
举个例子,假设有个循环要算10次加法:
for (int i = 0; i < 10; i++) {
#pragma HLS PIPELINE
c[i] = a[i] + b[i];
}
不加PIPELINE时,10次加法串行执行,需要10个时钟周期。加了之后,如果加法器是1拍延迟,那么第1拍算c[0],第2拍算c[1]的同时c[0]的结果已经出来了——总时间从10拍变成10+1-1=10拍?不对,实际上是10+1-1=10拍? 等等,我算一下:第一次迭代需要1拍,之后每1拍出一个结果,总共10拍。但如果不加流水线,也是10拍?
这里要注意:流水线的优势在迭代次数大、且每拍有多个操作时才能体现。如果只是单周期加法,流水线确实没优势。但如果是多周期操作(比如乘法、除法、内存访问),流水线就能大幅提升吞吐量。
3.2 II(Initiation Interval)——流水线的节拍
II,全称Initiation Interval,中文叫「启动间隔」。说白了就是:每隔多少拍,可以启动一次新的迭代。
我刚开始学HLS时,一直搞不清II和延迟的区别。后来在做一个视频处理项目时,终于明白了:
- 延迟(Latency):一次迭代从开始到结束需要多少拍
- II(Initiation Interval):两次迭代开始之间间隔多少拍
举个例子,假设一个函数延迟是5拍,II=1:
- 第0拍:启动第1次迭代
- 第1拍:启动第2次迭代(第1次还在执行)
- 第2拍:启动第3次迭代
- ... 第5拍:第1次迭代出结果
- 第6拍:第2次迭代出结果
你看,虽然每次迭代要5拍,但每1拍都能启动一次新的迭代,吞吐量提升了5倍!
3.3 流水线深度——到底能重叠多少层?
流水线深度,就是一次迭代的延迟(Latency)。它决定了最多能有多少次迭代同时进行。
还是刚才的例子,延迟5拍,II=1,那么最多同时有5次迭代在执行。这个「5」就是流水线深度。
我记得有一次做图像滤波,流水线深度达到了20多拍。当时我担心资源不够,结果综合后发现——流水线深度大并不一定消耗更多资源,因为硬件资源是共享的,只是寄存器多了些。
| 参数 | 含义 | 影响 |
|---|---|---|
| II=1 | 每拍启动一次新迭代 | 吞吐量最大,但可能有资源冲突 |
| II=2 | 每两拍启动一次新迭代 | 吞吐量减半,但资源冲突减少 |
| II可变 | 由编译器自动决定 | 默认行为,通常II≥1 |
3.4 自动流水线——让编译器帮你干活
Vivado HLS有个很贴心的功能:自动流水线。你可以在函数或顶层设置:
#pragma HLS PIPELINE II=1
或者更简单:
#pragma HLS PIPELINE
不指定II时,编译器会尝试II=1,如果做不到,就自动增大II。我一般会先不指定II,看看综合报告,再手动优化。
3.5 知识体系总览
下面这张图,是我自己总结的流水线知识体系。你看一眼就能明白各个概念之间的关系:
3.6 实战建议
最后,分享几个我在项目中总结的经验:
- 先不加PIPELINE,看综合报告——了解默认的延迟和资源,再决定是否加流水线
- II=1是目标,但不是必须——如果资源冲突严重,II=2甚至II=3也可以接受
- 注意循环体内的数据依赖——比如a[i]=a[i-1]+1这种,II会被迫增大
- 数组分割配合流水线——如果循环访问数组不同元素,用ARRAY_PARTITION可以降低II
assert(ap_cycle_count == expected_cycles)。这样不用等综合,就能快速验证。
好了,流水线的基础语法就讲到这里。记住:PIPELINE是HLS性能优化的第一把钥匙。下一节我们会深入讲循环展开和数组优化,到时候你会看到,这些技术配合流水线使用,效果更佳。