前言
流水线设计模式(Pipeline Pattern)是一种通过分段逻辑电路来提高系统吞吐率和工作频率的设计方法。它将复杂的任务分解为多个子步骤,每个步骤处理数据后传递给下一个步骤,直到所有步骤完成处理
请不要管前面的屁话,搞些专业名词云遮雾罩。
在可并行的程序中,流水线设计可以将一个共需要 5 步顺序运行的工作,压缩到只需要 1 步的时间(5 并行)。设计,很神奇吧。
最近给同伙讲解流水线,画了一个还不错的 excalidraw ,不用在博客上太浪费了。
讨论场景
让我们假设这样一个计算过程,有一系列初始条件,随后基于一个随机条件条件迭代计算。其中的 +1,只是意味着需要基于原条件做一个消耗算力和时间的操作。
让我们再假设这样一种硬件设施,可供 5 线程并行计算,方便后面的讲述。
串行操作
如果将上述计算过程写成 C 语言单线程运行,那么显然类似如下,横轴看作时钟。
其中的 1、2、3、4、5 赛道可以看作上面的计算步骤。 在这个简单的示例下也可以看作用于并行运行的 CPU 核心,核心1 专注于 线程A,核心2 专注于 线程B......
可能有聪明的小伙伴要问了,既然是 CPU 核心,那应该可以线程调度啊?怎么能让核心闲着,跑满!必须跑满!
但是观察我们的讨论场景,线程B 的运行是需要等待 线程A 出结果的,线程C 的运行需要等待 线程B 出结果......
所以只能放任 CPU 摸鱼了吗?
并行操作
与串行操作一样,预先计算5次迭代,挑选出其中的部分值,作为并行操作的基础。
随后的每个计算周期就可以并行计算啦,CPU 满满当当。
另一个讨论场景
以上是一个小模块的串行转并行的思路。
让我们另外假设一个更科学的计算过程,来讨论所谓的流水线在自迭代中的可行性。这种是更大众意义上的迭代。
在这种场景下,因为自迭代的结果作为下一步的初始条件,完全没有流水线式优化的空间。如果按照我们此前的并行优化,将所有横线叠在一起,那么 线程A 应该基于5个周期后 线程E 的结果,需要对未来进行预测,如果增加这样的算法,反而会拖慢运行效率。
| 时间步 | 线程1 (A) | 线程2 (B) | 线程3 (C) | 线程4 (D) | 线程5 (E) | 依赖关系说明 |
|---|---|---|---|---|---|---|
| t=1 | Iter1.A | — | — | — | — | Iter1.A依赖Iter0.E=0(初始条件) |
| t=2 | — | Iter1.B | — | — | — | 线程1需等待Iter1.E完成才能继续 |
| t=3 | — | — | Iter1.C | — | — | |
| t=4 | — | — | — | Iter1.D | — | |
| t=5 | — | — | — | — | Iter1.E | |
| t=6 | Iter2.A | — | — | — | — | Iter2.A依赖Iter1.E(此时已就绪) |
| t=7 | — | Iter2.B | — | — | — | 线程2需等待Iter2.A完成 |
| t=8 | — | — | Iter2.C | — | — | |
| t=9 | — | — | — | Iter2.D | — | |
| t=10 | — | — | — | — | Iter2.E | |
| t=11 | Iter3.A | — | — | — | — | Iter3.A依赖Iter2.E |
总结
考虑运算模块的输入输出,如果运算模块的输入来自于外部,那么适用流水线的并行化;如果运算模块是自迭代的,则不适用。
后记
一个月后回味到这个所谓的提高吞吐率的意思了,意思就是在多并行程序里能够提高数据 IO 的效率,相当于有每级的内部队列使得分步都可以独立排队。
俺寻思这不是很正常的思路吗?软件的模块化和拼装,本来每个子模块就应该自成体系独立运行,只有数据 IO,那么你拼装起来也是并行的程序。这种思路不仅在 FPGA 里,在一切软件设计里都是基础思路和指导思想。搞些专业术语让人看不懂,其实我在此之前也只是对流水线设计略有耳闻。