上午在论坛看到个热帖,里头的题目挺有意思的,简单的记录了一下。面试
在FPGA上实现一个模块,求32个输入中的最大值和次大值,32个输入由一个时钟周期给出。(题目来自论坛,面试题,若是以为不合适请留言删除)算法
从我我的的观点来看,这是一道很好的面试题目:数组
固然,输入的位宽可能会影响最终的解题思路和最终的实现可能性。但位宽在必定范围内,譬如8或者32,解题的方案应该都是一致的,只是会影响最终的频率。后文针对这一题目作具体分析。(题目没有说明重复元素如何处理,这里认为最大值和次大值能够是同样的,即计算重复元素)机器学习
从算法自己来看,找最大值和次大值的过程很简单;经过两次遍历:第一次求最大值,第二次求次大值; 算法复杂度是O(2n)。FPGA显然不可能在一个周期内完成如此复杂的操做,通常须要流水设计。这一方法下,整个结构是这样的ide
这种解法有若干个缺点,包括:延迟求最大值和次大值分别须要5clk延时,总延迟会超过10个cycles;资源占用较高,维持最大值坐标和清零操做耗费了较多资源,同时为了计算次大值,须要将输入寄存若干个周期,寄存器消耗较多。oop
另外一个种思路考虑同时求最大值和次大值,因为这一逻辑较为复杂,能够将其流水化,以下图。(以8输入为例,32输入须要增长两级)学习
其中sort模块完成对4输入进行排序,获得最大值和次大值输出的功能。4个数的排序较为复杂,这一过程大概须要2-3个cycles完成。对于32输入而言,输入数据通过32-16-8-4-2输出获得结果,延迟大概也有10个周期。测试
若是须要在FPGA上实现一个特定的算法,那么去找一个合适的方法去实现就行了;但若是是要实现一个特定的功能,那么须要找一个优秀的且适合FPGA实现的方法。优化
求最大值和次大值是一个很不彻底的排序,经过简单的查找复杂度为O(2n),且不利于硬件实现。对于排序而言,不管快速排序或者归并排序都用了分治的思想,若是咱们试图用分治的思想来解决这一问题。考虑当只有2个输入时,经过一个比较就能够获得输出,此时获得的是一个长度为2的有序数组。若是两个有序数组,那么经过两次比较就能够获得最大值和次大值。采用归并排序的思想,查找最大值和次大值的复杂度为O(1.5n)(即为n/2+n/2+n/4… ,不知道有没有算错)。采用归并排序的思想,从算法时间复杂度上看更为高效了。spa
那么这一方案是否适合FPGA实现呢,答案是确定的。分治的局部性适合FPGA的流水实现,框图以下。(以8输入为例,32输入须要增长两级)
其中meg模块内部有两级的比较器,通常而言1clk就能够完成,输入数据通过32-32-16-8-4-2获得结果,延迟为5个时钟周期。实现代码以下
module test#( parameter DW = 8 ) ( input clk, input [32*DW-1 :0] din, output [DW-1:0] max1, output [DW-1:0] max2 ); wire[DW-1:0] d[31:0]; generate genvar i; for(i=0;i<32;i=i+1) begin:loop_assign assign d[i] = din[DW*i+DW-1:DW*i]; end endgenerate // stage 1,comp reg[DW-1:0] s1_max[15:0]; reg[DW-1:0] s1_min[15:0]; generate for(i=0;i<16;i=i+1) begin:loop_comp always@(posedge clk) if(d[2*i]>d[2*i+1])begin s1_max[i] <= d[2*i]; s1_min[i] <= d[2*i+1]; end else begin s1_max[i] <= d[2*i+1]; s1_min[i] <= d[2*i]; end end endgenerate // stage 2, wire[DW-1:0] s2_max[7:0]; wire[DW-1:0] s2_min[7:0]; generate for(i=0;i<8;i=i+1) begin:loop_megs2 meg u_s2meg( .clk(clk), .g1_max(s1_max[2*i]), .g1_min(s1_min[2*i]), .g2_max(s1_max[2*i+1]), .g2_min(s1_min[2*i+1]), .max1(s2_max[i]), .max2(s2_min[i]) ); end endgenerate // stage 3, wire[DW-1:0] s3_max[3:0]; wire[DW-1:0] s3_min[3:0]; generate for(i=0;i<4;i=i+1) begin:loop_megs3 meg u_s3meg( .clk(clk), .g1_max(s2_max[2*i]), .g1_min(s2_min[2*i]), .g2_max(s2_max[2*i+1]), .g2_min(s2_min[2*i+1]), .max1(s3_max[i]), .max2(s3_min[i]) ); end endgenerate // stage 4, wire[DW-1:0] s4_max[1:0]; wire[DW-1:0] s4_min[1:0]; generate for(i=0;i<2;i=i+1) begin:loop_megs4 meg u_s4meg( .clk(clk), .g1_max(s3_max[2*i]), .g1_min(s3_min[2*i]), .g2_max(s3_max[2*i+1]), .g2_min(s3_min[2*i+1]), .max1(s4_max[i]), .max2(s4_min[i]) ); end endgenerate // stage 5, meg u_s5meg( .clk(clk), .g1_max(s4_max[0]), .g1_min(s4_min[0]), .g2_max(s4_max[1]), .g2_min(s4_min[1]), .max1(max1), .max2(max2) ); endmodule module meg#( parameter DW = 8 ) ( input clk, input [DW-1 :0] g1_max, input [DW-1 :0] g1_min, input [DW-1 :0] g2_max, input [DW-1 :0] g2_min, output reg [DW-1:0] max1, output reg [DW-1:0] max2 ); always@(posedge clk) begin if(g1_max>g2_max) begin max1 <= g1_max; if(g2_max>g1_min) max2 <= g2_max; else max2 <= g1_min; end else begin max1 <= g2_max; if(g1_max>g2_min) max2 <= g1_max; else max2 <= g2_min; end end endmodule
简单测试了上面的代码,在上一代器件上(20nm FPGA),8bit数据输入模块能综合到很高的频率,逻辑级数大概是5级左右,对于整个工程而言瓶颈基本不会出如今这一部分。32bit数据输入因为数据位宽太大,频率不会过高,可是经过将meg模块作一级流水,也几乎不会成为整个系统的瓶颈。
32bit32输入状况下,数据输入位宽为1024(不是IO输入,是内部信号)。以前在通讯/数字信号处理方面可能不会用到这么大位宽的数据,但对于AI领域FPGA的应用,数千比特的输入应该是很日常的,这的确会影响最终FPGA上实现的效果。要想让机器学习算法在FPGA上跑得更好,还须要算法和FPGA共同努力才是。