基于XC7Z100+OV5640(DSP接口)YOLO人脸识别前向推理过程(部分5)
Stream_rx模块代码编写
-
功能
- Stream_rx模块是一个用来接收PS端发送的数据(包括权重、偏置、输入数据、激活查找表等)的模块,需要完成两个功能:
- 完成对DMA数据的接收功能,并且区分当前接收的是哪一种类型的数据(根据data_type寄存器判断)。
- 产生write_finish信号,给到main_control模块,表示接收完成。
- Stream_rx模块需要根据Stream接口的时序关系来进行数据接收,主要涉及到t_valid、t_data、t_last、t_ready等信号。
- Stream_rx模块需要根据main_control模块发送的Axi4-lite信息来进行数据接收,主要涉及到write_start、data_type等寄存器。
- Stream_rx模块需要将接收到的数据输出到相应的缓存模块,并且产生相应的value信号(如feature_value、weight_value等),表示数据类型和有效性。
- Stream_rx模块需要根据t_last和t_valid信号产生write_finish信号,并且只保持一个时钟周期。
- Stream_rx模块是一个用来接收PS端发送的数据(包括权重、偏置、输入数据、激活查找表等)的模块,需要完成两个功能:
-
stream RX 模块有以下端口:
- stream接口:stream接口是一种高速数据传输接口,主要由tvalid、tdata、tlast、tkeep和tready五个信号组成,其中tvalid表示数据有效,tdata表示数据内容,tlast表示最后一个数据,tkeep表示掩码,tready表示接收端准备好。用于与PS端进行数据传输。
- data type判断:data type判断是指根据data type信号的值来判断当前接收的是哪种类型的数据,并产生相应的value信号来标识数据类型。用于从main_control模块接收当前数据的类型(feature data(输入数据)、weight data(权重数据)、bias data(偏置数据)、activation data(激活查找表数据))。
- write finish信号:写完成信号,用于表示stream RX模块已经接收完所有的数据,并通知main control模块
- data信号,给到后续的缓存模块,表示接收到的数据值(表示接收完成),产生一个高脉冲。
- value信号,用于输出不同类型数据的有效标志(feature_value, weight_value, bias_value, activation_value)给到后续的缓存模块,表示当前接收到的数据类型,只有一个为高,其他为低。
-
stream RX 模块的代码编写思路
- 首先,定义模块的端口,包括stream接口的信号(tvalid, tdata, tlast, tkeep, tready),data type信号(表示数据类型),write finish信号(表示数据接收完成),以及不同类型数据的有效标志信号(feature value, weight value, bias value, activation value)。
- 其次,根据stream接口的时序关系,使用state信号来控制tready信号的产生和拉低。state信号是从main control模块传来的,表示当前模块的状态。当state为write状态时,表示PL端准备好接收数据,此时将tready拉高;当state为其他状态时,表示PL端不需要接收数据,此时将tready拉低。
- 然后,根据tvalid, tlast和tready信号的握手,产生write finish信号,并拉高一个时钟周期。write finish信号是用来通知main control模块数据接收完成的。当tvalid, tlast和tready同时为高时,表示最后一个数据已经接收,此时将write finish拉高;当write finish为高时,将其延迟一个时钟周期后再拉低。
- 最后,根据data type信号,产生不同类型数据的有效标志信号,并输出tdata信号。data type信号是从PS端通过Axi4-lite寄存器传来的,表示当前发送的数据类型。根据data type的值,将对应类型数据的有效标志信号拉高,其他类型数据的有效标志信号拉低;同时将tdata信号输出给后续模块。这样可以区分当前接收的是哪种类型的数据,并进行相应的缓存或处理。
-
主要代码:**
// Stream Rx Axi4-Stream接收端口 input [63:0] s_axis_mm2s_tdata , // 接收数据 input [ 7:0] s_axis_mm2s_tkeep , // 接收有效位 input s_axis_mm2s_tvalid , // 接收有效信号 output wire s_axis_mm2s_tready , // 接收就绪信号 input s_axis_mm2s_tlast , // 接收结束信号
// Main Ctrl 主控制模块 input [ 1:0] data_type , // 数据类型信号,用于区分特征、权重、偏置和激活函数参数 input [ 5:0] state , // 状态机信号,用于控制模块的运行状态 output wire write_finish , // 写入完成信号,用于通知主控制模块数据写入内存完成
// value信号 output wire [63:0] stream_rx_data , // 接收数据缓存,用于暂存接收到的数据并传递给后续模块 output wire stream_feature_vld , // 特征数据有效信号,用于标识输出的数据是否为特征数据 output wire stream_weight_vld , // 权重数据有效信号,用于标识输出的数据是否为权重数据 output wire stream_bias_vld , // 偏置数据有效信号,用于标识输出的数据是否为偏置数据 output wire stream_leakyrelu_vld // 激活函数参数有效信号,用于标识输出的数据是否为激活函数参数
//主要代码 assign s_axis_mm2s_tready = state[1]; // 将接收就绪信号赋值为状态机的第二位,表示在写入状态时才接收数据 assign write_finish = s_axis_mm2s_tlast & s_axis_mm2s_tvalid; // 将写入完成信号赋值为接收结束信号和接收有效信号的逻辑与,表示在一帧数据结束且有效时才写入完成 assign stream_data_vld = s_axis_mm2s_tvalid & s_axis_mm2s_tready; // 将接收数据有效信号赋值为接收有效信号和接收就绪信号的逻辑与,表示在接收到有效数据且准备好接收时才输出有效 assign stream_rx_data = s_axis_mm2s_tdata; // 将接收数据缓存赋值为接收数据端口,表示直接将接收到的数据传递给后续模块 assign stream_feature_vld = (data_type == FEATURE_DATA) ? stream_data_vld : 1'b0; // 将特征数据有效信号赋值为一个三目运算符,表示如果当前的数据类型是特征类型,则输出接收数据有效信号,否则输出无效 assign stream_weight_vld = (data_type == WEIGHT_DATA) ? stream_data_vld : 1'b0; // 将权重数据有效信号赋值为一个三目运算符,表示如果当前的数据类型是权重类型,则输出接收数据有效信号,否则输出无效 assign stream_bias_vld = (data_type == BIAS_DATA) ? stream_data_vld : 1'b0; // 将偏置数据有效信号赋值为一个三目运算符,表示如果当前的数据类型是偏置类型,则输出接收数据有效信号,否则输出无效 assign stream_leakyrelu_vld = (data_type == LEAKYRELU_DATA) ? stream_data_vld : 1'b0; // 将激活函数参数有效信号赋值为一个三目运算符,表示如果当前的数据类型是激活函数参数类型,则输出接收数据有效信号,否则输出无效
例化:
// 主控制模块实例化,负责控制整个加速器的运行流程和状态转换 main_ctrl U1_main_ctrl_inst( // system signals 系统信号 .sclk (sclk ), // 连接时钟信号 .s_rst_n (s_rst_n ), // 连接复位信号 // Axi4-Lite Reg Axi4-Lite寄存器 .slave_lite_reg0 (slave_lite_reg0 ), // 连接从设备寄存器0,用于获取配置参数 .slave_lite_reg1 (slave_lite_reg1 ), // 连接从设备寄存器1,用于获取配置参数 .slave_lite_reg2 (slave_lite_reg2 ), // 连接从设备寄存器2,用于获取配置参数 .slave_lite_reg3 (slave_lite_reg3 ), // 连接从设备寄存器3,用于获取配置参数 // .state (state ), // 输出状态机信号,用于控制后续模块的运行状态 .data_type (data_type ), // 输出数据类型信号,用于控制后续模块的数据类型 .write_finish (write_finish ), // 输入写入完成信号,用于判断数据是否写入内存完成 .read_finish (read_finish ), // 输入读取完成信号,用于判断数据是否从内存读出完成 .conv_finish (conv_finish ), // 输入卷积完成信号,用于判断卷积运算是否完成 .upsample_finish (upsample_finish ), // 输入上采样完成信号,用于判断上采样运算是否完成 .task_finish (task_finish ) // 输出任务完成信号,用于向外部发送中断信号 );
// 接收模块实例化,负责接收Axi4-Stream数据并分发给后续模块 stream_rx U2_stream_rx_inst( // system signals 系统信号 .sclk (sclk ), // 连接时钟信号 .s_rst_n (s_rst_n ), // 连接复位信号 // Stream Rx Axi4-Stream接收端口 .s_axis_mm2s_tdata (s_axis_mm2s_tdata ), // 连接接收数据端口 .s_axis_mm2s_tkeep (s_axis_mm2s_tkeep ), // 连接接收有效位端口 .s_axis_mm2s_tvalid (s_axis_mm2s_tvalid ), // 连接接收有效信号端口 .s_axis_mm2s_tready (s_axis_mm2s_tready ), // 输出接收就绪信号端口,用于控制数据流的速率 .s_axis_mm2s_tlast (s_axis_mm2s_tlast ), // 连接接收结束信号端口,用于判断一帧数据是否结束 // Main Ctrl 主控制模块 .data_type (data_type ), // 输入数据类型信号,用于区分不同类型的数据 .state (state ), // 输入状态机信号,用于控制模块的运行状态 .write_finish (write_finish ), // 输出写入完成信号,用于通知主控制模块数据写入内存完成 // .stream_rx_data (stream_rx_data ), // 输出接收数据缓存,用于暂存接收到的数据并传递给后续模块 .stream_feature_vld (stream_feature_vld ), // 输出特征数据有效信号,用于标识输出的数据是否为特征数据 .stream_weight_vld (stream_weight_vld ), // 输出权重数据有效信号,用于标识输出的数据是否为权重数据 .stream_bias_vld (stream_bias_vld ), // 输出偏置数据有效信号,用于标识输出的数据是否为偏置数据 .stream_leakyrelu_vld (stream_leakyrelu_vld ) // 输出激活函数参数有效信号,用于标识输出的数据是否为激活函数参数 );
bias缓存模块代码编写
BIAS缓存模块的功能
- 缓存卷积计算所需的BIAS参数
- 当PS传输BIAS数据至PL时,对其进行缓存
- 当PL内部加速器需使用相应BIAS数据时,读出
BIAS缓存模块的注意点
- 当前加速器方案会同时计算8个卷积通道
- 卷积计算时要保证BIAS缓存模块能同时输出8通道的BIAS
- BIAS数据位宽为32比特,通过64比特的stream data传输
- 每个stream data包含两个BIAS数据,高32比特和低32比特
BIAS缓存模块的编写思路
考虑以下几个问题:
- 怎么写RAM?即如何将PS传输过来的bias数据存储到RAM中。
- 怎么读RAM?即如何从RAM中读取需要的bias数据。
- RAM的位宽和深度如何设置?即如何确定RAM的大小和容量。
- 如何保证RAM能同时输出8通道的bias数据?即如何满足加速器的并行计算需求。
-
- bias缓存模块的功能是缓存卷积计算所需的bias参数,当PS传输bias数据至PL时,对其进行缓存,当PA内部加速器需要使用相应bias数据时,读出。 - bias缓存模块选择RAM IP来充当缓存的主体,因为RAM IP可以实现高速的读写操作,并且可以自定义位宽和深度。 - bias缓存模块需要考虑如何写RAM和如何读RAM两个方面。
-
怎么写RAM?
- 首先,需要了解bias数据是如何通过DMA传输给PL的。bias数据是以32位的INT32类型存储在bin文件中,每个bin文件对应一层卷积的所有通道的bias参数。DMA每次发送64位的数据,也就是两个bias参数。因此,每个64位的数据包含两个通道的bias参数。
- 其次,需要考虑如何将64位的数据写入RAM。当前加速器方案会同时计算8个转接通道,所以需要保证bias缓存模块能同时输出8个通道的bias参数。因此,不能直接使用一个64位宽度的RAM,而是需要使用4个32位宽度的RAM,并且按照一定的规则将64位的数据分配给不同的RAM。
- 具体来说,每次接收到一个64位的数据时,将高32位写入RAM0,低32位写入RAM1;接收到第二个64位的数据时,将高32位写入RAM2,低32位写入RAM3;以此类推。这样做的目的是为了让每个RAM存储相邻两个通道的bias参数,方便后续读取。
- 最后,需要考虑如何控制RAM的写地址和写使能信号。可以使用一个计数器data_cnt来记录接收到的64位数据的个数,并且每隔4个数据就加1。这样,data_cnt就可以作为RAM的写地址,因为每个RAM只需要存储128个bias参数(假设最大通道数为1024)。同时,可以使用data_cnt的低两位来控制不同RAM的写使能信号。具体来说,当data_cnt为0时,使能RAM0和RAM1;当data_cnt为1时,使能RAM2和RAM3;当data_cnt为2时,使能RAM0和RAM1;当data_cnt为3时,使能RAM2和RAM3。
-
怎么读RAM?
- 首先,需要了解如何从PS端获取读地址。在main_control模块中有一个bias_addr信号来指定当前计算哪8个转接通道时需要获取哪8个通道的bias参数。这个信号是由PS端控制的,并且传递给bias缓存模块。
- 其次,需要考虑如何从4个RAM中读取8个通道的bias参数。可以使用同一个读地址来读取4个RAM,并且将4个RAM的输出拼接成一个64位的数据。这样就可以同时输出8个通道的bias参数了。
- 最后,需要考虑如何控制RAM的读使能信号。在生成RAM IP时可以选择不使用读使能信号,而是直接给一个时钟信号,让RAM一直处于读状态。这样可以简化RAM的控制逻辑,并且不影响读取的时序。
-
RAM的位宽和深度如何设置?
- 首先,需要了解每个通道的bias参数占用多少位。每个通道的bias参数是32位的整数。因此,RAM的位宽应该设置为32位。其次,需要了解一共有多少个通道的bias参数。根据当前网页内容,最多有1024个通道的bias参数。因此,RAM的深度应该设置为1024除以8等于128。
-
如何保证RAM能同时输出8通道的bias数据?
- 首先,需要了解加速器为什么需要同时输出8通道的bias数据。加速器采用了一种方案,同时计算8个转接通道的卷积结果。因此,在进行卷积计算时,需要使用对应8个转接通道的bias数据。其次,需要了解如何设计RAM来实现这一需求。一种可行的方案是使用4个RAM来存储所有通道的bias数据,每个RAM存储两个通道的bias数据,然后将4个RAM的输出拼接起来,形成一个64位的输出端口。这样,每次读取RAM时,都能得到8个通道的bias数据。
主要代码:
assign wr_addr = data_cnt[8:2];
assign ram_sel = data_cnt[1:0];
assign ram0_wr_en = (ram_sel == 'd0) ? stream_bias_vld : 1'b0;
assign ram1_wr_en = (ram_sel == 'd1) ? stream_bias_vld : 1'b0;
assign ram2_wr_en = (ram_sel == 'd2) ? stream_bias_vld : 1'b0;
assign ram3_wr_en = (ram_sel == 'd3) ? stream_bias_vld : 1'b0;
always @(posedge sclk or negedge s_rst_n) begin
if(s_rst_n == 1'b0)
data_cnt <= 'd0;
else if(stream_bias_vld == 1'b1)
data_cnt <= data_cnt + 1'b1;
else
data_cnt <= 'd0;
end
例化
bias_ram_ip U3_bias_ram_ip_inst (
.clka (sclk ), // input wire clka
.wea (ram3_wr_en ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [6 : 0] addra
.dina (stream_rx_data ), // input wire [63 : 0] dina
.clkb (sclk ), // input wire clkb
.addrb (bias_rd_addr ), // input wire [6 : 0] addrb
.doutb ({bias_ch7, bias_ch6} ) // output wire [63 : 0] doutb
);
- bias缓存模块的功能是缓存卷积计算所需的bias参数,当PS传输bias数据至PL时,对其进行缓存,当PA内部加速器需要使用相应bias数据时,读出。
- bias缓存模块选择RAM IP作为缓存的主体,使用双端口RAM,位宽为64位,深度为128。
- bias数据是通过DMA以64位的stream data传输过来的,每个stream data包含两个32位的bias数据。因此,需要用四个RAM来存储8个通道的bias数据,每个RAM存储两个通道的bias数据。
- RAM的写操作是根据stream data的valid信号和data cnt信号进行的。data cnt信号是一个计数器,每接收到一个有效的stream data就加一,每隔四个stream data就清零。RAM的写地址是根据data cnt信号的高7位产生的,每隔四个stream data就加一。RAM的写使能信号是根据data cnt信号的低2位产生的,每个RAM对应一个写使能信号,当data cnt信号为0、1、2、3时,分别使能RAM 0、1、2、3。
- RAM的读操作是根据PS端传来的bias addr信号进行的。bias addr信号是一个8位的信号,用来指定需要读取哪8个通道的bias数据。四个RAM共用同一个bias addr信号,并且不需要读使能信号。RAM的输出数据是64位的,包含两个通道的bias数据。需要将四个RAM的输出数据拼接起来,形成256位的输出数据,包含8个通道的bias数据。
激活模块代码编写
-
激活模块的作用和原理
- 利用查找表实现激活处理
- 将输入数据当成缓存的地址值
- 输出即为缓存中对应地址的激活函数值
- 缓存激活函数查找表数据
- 256个8位无符号整形数据,可以用一个8位宽、256深度的RAM来存储
- 保存为bin文件并通过PS发送到PL
- 利用查找表实现激活处理
-
激活模块的实现
-
由于DMA传输的数据是64位宽,每次可以传输8个激活函数值,所以需要将64位数据分成8个8位数据,然后依次写入RAM。
-
设置 RAM 的深度、位宽和地址
- 写入时:深度为32,位宽为64,地址为5位
- 读取时:深度为256,位宽为8,地址为8位
-
解释写入和读取 RAM 的时序和方法
-
写入RAM时:需要根据value信号来控制写地址的自增。写地址的范围是0-31,因为每次写入64位数据相当于写入了8个地址。当写地址到达31时,需要清零。
- 为了解决写入速度不够的问题,可以使用双端口RAM,设置写端口为64位宽、32深度,读端口为8位宽、256深度。这样可以一次写入64位数据,然后按照8位数据读出。
-
读出RAM时:需要将卷积层的输出作为读地址。由于卷积层同时输出8个通道的数据,所以需要复制8个RAM来分别对应每个通道。每个RAM的读地址都是由对应通道的卷积输出决定的。
- 为了保证读出的数据顺序和写入的数据顺序一致,需要注意RAM的地址映射关系。例如,写入时第一个激活函数值对应的地址是0,读出时也应该是0。
-
-
由于卷积计算时同时计算8个通道,所以需要同时访问RAM的8个地址。为了实现这一点,可以将RAM实例化8次,每个RAM保存同一份数据,然后根据不同通道的卷积输出作为不同RAM的读地址。
-
为了生成输出的有效标志信号,可以根据RAM的读延时来设置。例如,如果RAM的读延时是两个时钟周期,那么可以将读使能信号延迟两个时钟周期作为输出有效标志信号。
-
由于RAM有两个时钟周期的读延时,所以需要在读使能信号后延时两个时钟周期来产生输出有效信号。输出有效信号表示激活模块已经输出了正确的激活函数值。
-
-
激活模块的注意事项
- 需要根据DMA传输的64位数据来拆分成8个8位数据
- 需要根据RAM IP的读延时来设置输出有效标志
- 需要等待卷积模块的输出后再进行初始化
input [ 7:0] ch0_data_i ,
input [ 7:0] ch1_data_i ,
input [ 7:0] ch2_data_i ,
input [ 7:0] ch3_data_i ,
input [ 7:0] ch4_data_i ,
input [ 7:0] ch5_data_i ,
input [ 7:0] ch6_data_i ,
input [ 7:0] ch7_data_i ,
input ch_data_vld_i ,
//
output wire [ 7:0] ch0_data_o ,
output wire [ 7:0] ch1_data_o ,
output wire [ 7:0] ch2_data_o ,
output wire [ 7:0] ch3_data_o ,
output wire [ 7:0] ch4_data_o ,
output wire [ 7:0] ch5_data_o ,
output wire [ 7:0] ch6_data_o ,
output wire [ 7:0] ch7_data_o ,
output wire ch_data_vld_o
reg [ 4:0] wr_addr ;
reg [ 1:0] data_arr ;
always @(posedge sclk or negedge s_rst_n) begin
if(s_rst_n == 1'b0)
wr_addr <= 'd0;
else if(stream_leakyrelu_vld == 1'b1)
wr_addr <= wr_addr + 1'b1;
else
wr_addr <= 'd0;
end
always @(posedge sclk) begin
data_arr <= {data_arr[0], ch_data_vld_i};
end
assign ch_data_vld_o = data_arr[1];
leakyrelu_ram_ip U0_leakyrelu_ram_ip (
.clka (sclk ), // input wire clka
.wea (stream_leakyrelu_vld ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [4 : 0] addra
.dina (stream_rx_data ), // input wire [63 : 0] dina
.clkb (sclk ), // input wire clkb
.enb (ch_data_vld_i ), // input wire enb
.addrb (ch0_data_i ), // input wire [7 : 0] addrb
.doutb (ch0_data_o ) // output wire [7 : 0] doutb
);
leakyrelu_ram_ip U1_leakyrelu_ram_ip (
.clka (sclk ), // input wire clka
.wea (stream_leakyrelu_vld ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [4 : 0] addra
.dina (stream_rx_data ), // input wire [63 : 0] dina
.clkb (sclk ), // input wire clkb
.enb (ch_data_vld_i ), // input wire enb
.addrb (ch1_data_i ), // input wire [7 : 0] addrb
.doutb (ch1_data_o ) // output wire [7 : 0] doutb
);
leakyrelu_ram_ip U2_leakyrelu_ram_ip (
.clka (sclk ), // input wire clka
.wea (stream_leakyrelu_vld ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [4 : 0] addra
.dina (stream_rx_data ), // input wire [63 : 0] dina
.clkb (sclk ), // input wire clkb
.enb (ch_data_vld_i ), // input wire enb
.addrb (ch2_data_i ), // input wire [7 : 0] addrb
.doutb (ch2_data_o ) // output wire [7 : 0] doutb
);
leakyrelu_ram_ip U3_leakyrelu_ram_ip (
.clka (sclk ), // input wire clka
.wea (stream_leakyrelu_vld ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [4 : 0] addra
.dina (stream_rx_data ), // input wire [63 : 0] dina
.clkb (sclk ), // input wire clkb
.enb (ch_data_vld_i ), // input wire enb
.addrb (ch3_data_i ), // input wire [7 : 0] addrb
.doutb (ch3_data_o ) // output wire [7 : 0] doutb
);
leakyrelu_ram_ip U4_leakyrelu_ram_ip (
.clka (sclk ), // input wire clka
.wea (stream_leakyrelu_vld ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [4 : 0] addra
.dina (stream_rx_data ), // input wire [63 : 0] dina
.clkb (sclk ), // input wire clkb
.enb (ch_data_vld_i ), // input wire enb
.addrb (ch4_data_i ), // input wire [7 : 0] addrb
.doutb (ch4_data_o ) // output wire [7 : 0] doutb
);
leakyrelu_ram_ip U5_leakyrelu_ram_ip (
.clka (sclk ), // input wire clka
.wea (stream_leakyrelu_vld ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [4 : 0] addra
.dina (stream_rx_data ), // input wire [63 : 0] dina
.clkb (sclk ), // input wire clkb
.enb (ch_data_vld_i ), // input wire enb
.addrb (ch5_data_i ), // input wire [7 : 0] addrb
.doutb (ch5_data_o ) // output wire [7 : 0] doutb
);
leakyrelu_ram_ip U6_leakyrelu_ram_ip (
.clka (sclk ), // input wire clka
.wea (stream_leakyrelu_vld ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [4 : 0] addra
.dina (stream_rx_data ), // input wire [63 : 0] dina
.clkb (sclk ), // input wire clkb
.enb (ch_data_vld_i ), // input wire enb
.addrb (ch6_data_i ), // input wire [7 : 0] addrb
.doutb (ch6_data_o ) // output wire [7 : 0] doutb
);
leakyrelu_ram_ip U7_leakyrelu_ram_ip (
.clka (sclk ), // input wire clka
.wea (stream_leakyrelu_vld ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [4 : 0] addra
.dina (stream_rx_data ), // input wire [63 : 0] dina
.clkb (sclk ), // input wire clkb
.enb (ch_data_vld_i ), // input wire enb
.addrb (ch7_data_i ), // input wire [7 : 0] addrb
.doutb (ch7_data_o ) // output wire [7 : 0] doutb
);
权重缓存模块设计及代码编写
-
权重缓存模块的功能
- 缓存权重参数,当 PS 通过 DMA 把权重参数发送给 PL 端时,PL 内部会做一个缓存
- 当进行卷积计算时,能够同时输出 8 通道的卷积参数,因为方案设计为同时进行 8 通道的卷积计算
-
权重缓存模块只讨论 \(3*3\) 的卷积,不涉及 \(1*1\) 的卷积
-
权重缓存模块的设计思路
-
首先要了解权重参数在 PL 端内部进行计算时是怎么使用的
- 以 Layer 2 这一层的计算为例,输入数据有 16 个通道,每个通道尺寸是 \(208*208\),输出数据有 32 个通道
- 如果正常计算,每个输出通道会把输入的 16 个通道数据都送到里面去,每个输出通道里面会有 16 个 \(3*3\) 的卷积核
- 在 PL 端内部,每次只能同时计算 8 个输出通道,所以要分批次进行计算
- 在 PL 端内部,每个输出通道需要同时输出 8 个输入通道相关的 \(3*3\) 权重参数
- 单个输出通道内,需同时输出 8 个输入通道相关的权重参数
- 其他输出通道也是类似的情况
-
然后要了解 PS 通过 DMA 发送权重参数时是怎么发送的
- 可以用 Python 代码打印出权重参数的分布和顺序
- 权重参数是按照输出通道和输入通道的顺序存储和发送的,每个输出通道里面有与所有输入通道相关的卷积核
- DMA 每次发送一个数据就是一个字节(8 bit),所以可以一次发送一个权重参数(也是一个字节)
- DMA 每次发送一个数据就是一个字节(8 bit),所以可以一次发送一个权重参数(也是一个字节)
- DMA 发送权重参数时,每个字节里面有四位用来表示输出通道编号(0-31),四位用来表示输入通道编号(0-15)
- DMA 发送权重参数时,每八个字节就是一个完整的卷积核(3*3),每八个卷积核就是一个完整的输出通道(与所有输入通道相关)
- DMA 发送权重参数时,先发送与第一个输入通道相关的所有卷积核(32个),然后再发送与第二个输入通道相关的所有卷积核(32个),依次类推
- DMA 发送权重参数时,如果要切换到下一个输入通道或者下一个输出通道,就要改变相应的四位编号
权重参数是指卷积神经网络中的卷积核,它们是用来对输入数据进行特征提取的矩阵。权重参数通常需要在 PS 端进行训练和更新,然后在 PL 端进行推理和计算。
RAM 是指随机存取存储器,它是一种可读写的内存,可以用来缓存权重参数。RAM 的优点是访问速度快,缺点是容量有限,需要根据网络结构和层次进行分配。
PS 端通过 DMA 发送权重参数的方式(以RAM为例)可以分为以下几个步骤:
- PS 端将权重参数按照一定的顺序和格式保存为二进制文件,然后通过 SD 卡或者其他方式加载到 ZYNQ 的文件系统中。
- PS 端通过 DMA 驱动程序配置 DMA 的工作模式、源地址、目的地址、传输长度等参数,然后启动 DMA 传输。
- DMA 控制器从 PS 端的文件系统中读取权重参数,并将其发送到 PL 端的 RAM 中。每次发送的数据长度由 DMA 的位宽决定,例如 64 位或者 128 位。
- PL 端的 RAM 根据写地址和写使能信号接收并存储权重参数。每个 RAM 只存储一个输出通道的权重参数,需要多个 RAM 来实现多通道并行计算。
- PL 端的 RAM 根据读地址和读使能信号输出权重参数,并将其送入卷积计算模块。每个 RAM 可以同时输出多个输入通道的权重参数,例如 8 个或者 16 个。
-
最后要确定权重缓存模块内部的存储方案
-
使用 RAM 来存储权重参数,因为 RAM 可以提供读地址和写地址,方便控制
-
使用8个RAM分别存储8个输入通道的卷积权重参数,每个RAM的位宽为72比特,深度为256,可以同时输出一个3*3的卷积权重参数。
-
使用一个数据拼接单元将DMA传输过来的8比特的权重参数拼接成72比特,每9个8比特拼接成一个72比特。
-
使用一个数据计数器对value信号进行计数,每9个value信号产生一个写使能信号。
-
使用一个通道计数器对写使能信号进行计数,每8个写使能信号产生一个写地址自加信号。
-
使用一个写地址寄存器记录当前写入的RAM地址,每次写地址自加信号到来时,写地址加1。
-
使用一个选择器根据通道计数器的值选择当前要写入的RAM的写使能信号,每个RAM只有在对应的写使能信号到来时才进行写入操作。
-
使用一个读地址寄存器接收来自PH3的读地址信号,控制每个RAM的读操作。
-
将每个RAM的输出数据连接到输出总线上,形成8个72比特的卷积权重参数输出。
-
对于每个输出通道
- 需要用八个 RAM 分别存储与八个输入通道相关的权重参数
- 需要用一个单通道权重缓存模块来实现上述功能
- 需要用八个单通道权重缓存模块来实现并行计算
-
-
权重缓存模块内部存储方案设计(以RAM为例)如下:
权重缓存模块是用于在卷积神经网络中缓存权重参数的模块,它可以提高计算效率和减少内存开销。权重缓存模块的内部存储方案设计需要考虑以下几个方面:
- RAM的位宽和深度。位宽决定了RAM可以同时存储多少个权重参数,深度决定了RAM可以存储多少组权重参数。根据当前网页内容,位宽设置为72比特,深度设置为256。
- RAM的读写方式。读写方式决定了RAM如何接收和输出权重参数。读写方式都是72比特,即每次读写一个3x3的权重参数。
- RAM的数量和分配。数量和分配决定了RAM如何对应不同的输入通道和输出通道的权重参数。RAM的数量为8个,分别对应8个输出通道的权重参数。每个RAM内部按照输入通道的顺序存储16个3x3的权重参数。
- RAM的地址控制。地址控制决定了RAM如何根据计算需求选择不同的权重参数。地址控制需要用到两个计数器:一个是对value信号进行计数的data_cnt,另一个是对写使能进行计数的ch_cnt。data_cnt用于拼接数据和产生写使能信号,ch_cnt用于选择不同的RAM和增加写地址。
output wire [71:0] ch0_weight_3x3 ,
output wire [71:0] ch1_weight_3x3 ,
output wire [71:0] ch2_weight_3x3 ,
output wire [71:0] ch3_weight_3x3 ,
output wire [71:0] ch4_weight_3x3 ,
output wire [71:0] ch5_weight_3x3 ,
output wire [71:0] ch6_weight_3x3 ,
output wire [71:0] ch7_weight_3x3
wire ram0_wr_en ;
wire ram1_wr_en ;
wire ram2_wr_en ;
wire ram3_wr_en ;
wire ram4_wr_en ;
wire ram5_wr_en ;
wire ram6_wr_en ;
wire ram7_wr_en ;
assign ram0_wr_en = (ch_cnt == 'd0) ? wr_en : 1'b0;
assign ram1_wr_en = (ch_cnt == 'd1) ? wr_en : 1'b0;
assign ram2_wr_en = (ch_cnt == 'd2) ? wr_en : 1'b0;
assign ram3_wr_en = (ch_cnt == 'd3) ? wr_en : 1'b0;
assign ram4_wr_en = (ch_cnt == 'd4) ? wr_en : 1'b0;
assign ram5_wr_en = (ch_cnt == 'd5) ? wr_en : 1'b0;
assign ram6_wr_en = (ch_cnt == 'd6) ? wr_en : 1'b0;
assign ram7_wr_en = (ch_cnt == 'd7) ? wr_en : 1'b0;
always @(posedge sclk) begin
wr_data <= {weight_data_in, wr_data[71:8]};
end
weight_ram_ip ch0_weight_ram_ip (
.clka (sclk ), // input wire clka
.wea (ram0_wr_en ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [7 : 0] addra
.dina (wr_data ), // input wire [71 : 0] dina
.clkb (sclk ), // input wire clkb
.addrb (wbuffer_rd_addr ), // input wire [7 : 0] addrb
.doutb (ch0_weight_3x3 ) // output wire [71 : 0] doutb
);
weight_ram_ip ch1_weight_ram_ip (
.clka (sclk ), // input wire clka
.wea (ram1_wr_en ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [7 : 0] addra
.dina (wr_data ), // input wire [71 : 0] dina
.clkb (sclk ), // input wire clkb
.addrb (wbuffer_rd_addr ), // input wire [7 : 0] addrb
.doutb (ch1_weight_3x3 ) // output wire [71 : 0] doutb
);
weight_ram_ip ch2_weight_ram_ip (
.clka (sclk ), // input wire clka
.wea (ram2_wr_en ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [7 : 0] addra
.dina (wr_data ), // input wire [71 : 0] dina
.clkb (sclk ), // input wire clkb
.addrb (wbuffer_rd_addr ), // input wire [7 : 0] addrb
.doutb (ch2_weight_3x3 ) // output wire [71 : 0] doutb
);
weight_ram_ip ch3_weight_ram_ip (
.clka (sclk ), // input wire clka
.wea (ram3_wr_en ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [7 : 0] addra
.dina (wr_data ), // input wire [71 : 0] dina
.clkb (sclk ), // input wire clkb
.addrb (wbuffer_rd_addr ), // input wire [7 : 0] addrb
.doutb (ch3_weight_3x3 ) // output wire [71 : 0] doutb
);
weight_ram_ip ch4_weight_ram_ip (
.clka (sclk ), // input wire clka
.wea (ram4_wr_en ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [7 : 0] addra
.dina (wr_data ), // input wire [71 : 0] dina
.clkb (sclk ), // input wire clkb
.addrb (wbuffer_rd_addr ), // input wire [7 : 0] addrb
.doutb (ch4_weight_3x3 ) // output wire [71 : 0] doutb
);
weight_ram_ip ch5_weight_ram_ip (
.clka (sclk ), // input wire clka
.wea (ram5_wr_en ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [7 : 0] addra
.dina (wr_data ), // input wire [71 : 0] dina
.clkb (sclk ), // input wire clkb
.addrb (wbuffer_rd_addr ), // input wire [7 : 0] addrb
.doutb (ch5_weight_3x3 ) // output wire [71 : 0] doutb
);
weight_ram_ip ch6_weight_ram_ip (
.clka (sclk ), // input wire clka
.wea (ram6_wr_en ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [7 : 0] addra
.dina (wr_data ), // input wire [71 : 0] dina
.clkb (sclk ), // input wire clkb
.addrb (wbuffer_rd_addr ), // input wire [7 : 0] addrb
.doutb (ch6_weight_3x3 ) // output wire [71 : 0] doutb
);
weight_ram_ip ch7_weight_ram_ip (
.clka (sclk ), // input wire clka
.wea (ram7_wr_en ), // input wire [0 : 0] wea
.addra (wr_addr ), // input wire [7 : 0] addra
.dina (wr_data ), // input wire [71 : 0] dina
.clkb (sclk ), // input wire clkb
.addrb (wbuffer_rd_addr ), // input wire [7 : 0] addrb
.doutb (ch7_weight_3x3 ) // output wire [71 : 0] doutb
);