CNN

[CNN]4. MNIST Conv0_Layer

낑낑이 2025. 1. 19. 17:21

오늘은 실제 Conv 동작이 일어나게되는 Conv0Layer에 대해서 살펴보겠습니다

 
module Conv0_Layer#(
    parameter DW=16,
    parameter WBAW=4,
    parameter FBAW= 10,
    parameter Out_ch=16
)(
    input clk,
    input rst_n,
    input Active_flag,//Active flag는 Post Conv Top에서 Activation및 Maxpooling이 일어날때도 포함해야하기때문에 이를 포함시켜주는것것
    input [5:0] state,
    input Conv_start,// Convolution 연산을 시작하도록 지시하는 신호입니다.
    output active_done,
    input [DW*16-1:0] Weight_da,//입력 가중치 데이터. 가중치 번들 단위로 16개씩 묶여 있습니다.
    input [DW-1:0] Pix_da_in,//입력 픽셀 데이터. 폭은 DW (16비트)입니다.
    output Weight_rd_en,//가중치 데이터를 읽기 위해 활성화 신호를 출력합니다
    output Fmap_rd_en,//Input Featuremap의 data를 읽기위해 활성화 신호
    output Conv_done,//Conv 연산이 완료되었음을 나타내는 신호
    output [DW*16-1:0] Cram_da_out_bundle,//연산 결과로 생성된 출력 데이터 bundle
    output reg post_enable
    );

변수만 봐도 어떤거에 관련되어있는 값인지 잘 알 수 있는데 Cram_da_out_bundle에 대해서 설명하겠습니다

이처럼 Input Featuremap과 Kernel(Weight)가 곱해져 Output Featuremap이 생성됩니다

하지만 저번에 말씀드린것처럼 output Featuremap의 Channel은 Kernel의 Channel을 따라갑니다

이와같이 16개의 Channel에 대해서 수행하도록 만들어져있죠 다시 그림으로 나타내면

16개 다 그리기 귀찮아서 대충 16개라고 봐주세요

DataWidth만큼의 Bit를 가진 OutputFeaturemap 하나의 연산결과가 16개 쭉 있다고 생각하면됩니다 

하지만 효율적인 연산, 계산 Flow를위해 

이와같이 쭉 연결한다고 보면됩니다

    output [DW*16-1:0] Cram_da_out_bundle,//연산 결과로 생성된 출력 데이터 bundle

그래서 이 코드를 보면 [DW*16-1:0]로 16개의 Channel만큼 생성된것을 볼 수 있습니다

    assign Cram_da_out_bundle = {Cram_da_out[0], Cram_da_out[1], Cram_da_out[2], Cram_da_out[3],
                                  Cram_da_out[4], Cram_da_out[5], Cram_da_out[6], Cram_da_out[7],
                                  Cram_da_out[8], Cram_da_out[9], Cram_da_out[10], Cram_da_out[11],
                                  Cram_da_out[12], Cram_da_out[13], Cram_da_out[14], Cram_da_out[15]};

이와같이 하나의 Data로 묶기 위해 assign으로 선언해주게됩니다. 저 박스 하나하나가 Cram_da_out이 되는거죠

 

어떻게 Conv가 이루어지는지 알아보겠습니다

Conv동작원리

최상단에 선언된 

    input Conv_start,// Convolution 연산을 시작하도록 지시하는 신호입니다.
    input [DW*16-1:0] Weight_da,//입력 가중치 데이터. 가중치 번들 단위로 16개씩 묶여 있습니다.
    input [DW-1:0] Pix_da_in,//입력 픽셀 데이터. 폭은 DW (16비트)입니다.
    output Weight_rd_en,//가중치 데이터를 읽기 위해 활성화 신호를 출력합니다
    output Fmap_rd_en,//Input Featuremap의 data를 읽기위해 활성화 신호

해당 구문을 보겠습니다.

이는 Cntrl신호에서 Conv_start신호를 받으면 Weight_rd_en과 Fmap_rd_en 을 High 시켜 Featuremap data와 Weight data를 읽게됩니다.

Conv0는 이와같이 구성되어있습니다. 하나하나 살펴보면 Channel Array는 Channel에 대한 Conv 연산을 하게되는곳입니다

 

해당 MNIST 연산에서는 Featuremap이 Conv 이후에 줄어드는것을 방지하기 위해

Padding 1 Stride 1을 설정하도록 하였습니다. Control 쪽을 살펴보게되면 

module Conv_control(
    input clk,
    input rst_n,
    input Conv_start,
    input Active_flag,
    output reg active_done,
    output reg Fmap_rd_en,
    output reg [1:0] fifo_wr_en,
    output reg [1:0] fifo_rd_en,
    output reg Weight_valid,
    output reg Weight_rd_en,
    output reg Cram_rd_en,
    output reg Conv_out_valid,
    output reg Conv_done
    );
    always @ (posedge clk or negedge rst_n)
    begin
        if(!rst_n) begin
            Conv_done<=0;
            cnt<=0;
            state<=IDLE;
            active_done <= 0;
        end
        else begin
            case(state)
                IDLE : begin
                    Conv_done<=0;
                    cnt<=0;
                    if(Conv_start) begin
                        state <= K_READ;
                    end
                    else if(Active_flag) begin  //Active flag 활성화시 Activation을 해줘야하기때문에
                        state <= Active;
                    end
                    else
                        state<=state;                    
                end
                K_READ: begin
                    if(cnt==8) begin
                        cnt<=0;
                        state<=CONV;//3*3 Kernel을 읽어야하기때문에
                    end
                    else begin
                        cnt<=cnt+1;
                    end
                end
                CONV: begin
                    if(cnt==whole_read_cycle+4) begin
                        cnt<=0;
                        state<=CONV_DONE; // Conv 동작때 4cycle뒤에 연산이 완료되기 때문에 +4 cycle이후에 Conv_Done
                    end
                    else begin
                        cnt<=cnt+1;
                    end              
                end
                CONV_DONE: begin
                    Conv_done<=1; //Conv_done 신호를 외부에 인가하여, 외부 Ctrl 문에 Conv_done 신호를 줘 다음동작을 함
                    cnt<=0;
                    state<=Active;
                end
                Active: begin
                    Conv_done<=0;
                    if(cnt==783+3) begin // Input Featuremap의 크기가 28*28=784였기때문에 또한 Latency도 포함
                        active_done <= 1;
                        cnt<=0;
                        state<=IDLE;
                    end
                    else begin
                        cnt<=cnt+1;
                    end
                end  
                default: begin
                    Conv_done<=0;
                    cnt<=0;
                    state<=0;
                end
            endcase
        end
    end            
 

이렇게 state가 제어되도록하고 state가 K_READ일때 

    always @ (posedge clk or negedge rst_n)
    begin
        if(!rst_n) begin
            Weight_rd_en<=0;
            Weight_valid<=0;
        end
        else begin
            Weight_valid<=Weight_rd_en;
            if(state==K_READ) begin
                Weight_rd_en<=1;
            end
            else
                Weight_rd_en<=0;
        end
    end
   
 

이처럼 Weight_rd_en을 켜 Weight를 읽을수 있도록합니다. 그렇게 되면 Weight_data가 Channel Array에 인가되도록하는것입니다.

    generate
    for(i=0;i<16;i=i+1) begin : Systolic
        Systolic_Array Systolic_Array(.clk(clk), .rst_n(rst_n), .Pix_da0(fifo_out[1]), .Pix_da1(fifo_out[0]), .Pix_da2(Pix_da),
         .Wei_da(Weight_da[DW*(16-i)-1:DW*(15-i)]), .weight_valid(weight_valid), .Fmap_da(Fmap_da[i]));
    end
    endgenerate
 

Channel Array 내부에 있는 Systolic Array를 보게되면 

Wei_da가 들어오도록 되어있는데

Conv0_inBuff를보면

    assign Weight_da = {Weight_Out[0],Weight_Out[1],Weight_Out[2],Weight_Out[3],Weight_Out[4],Weight_Out[5],Weight_Out[6],Weight_Out[7],
                        Weight_Out[8],Weight_Out[9],Weight_Out[10],Weight_Out[11],Weight_Out[12],Weight_Out[13],Weight_Out[14],Weight_Out[15]};
 

이와같이 선언되어있습니다

즉 MSB가 Channel1에 대한 Weight 값이게 되므로 DW만큼의 Weight값(여기서는 16비트이므로 ffff 같이 하위 4비트를 읽음) 그 Weight 값이 

module Systolic_Array#(
    parameter DW=16
)(
    input clk,
    input rst_n,
    input signed [DW-1:0] Pix_da0,
    input signed [DW-1:0] Pix_da1,
    input signed [DW-1:0] Pix_da2,
    input signed [DW-1:0] Wei_da,
    input weight_valid,
    output reg signed [DW-1:0] Fmap_da
    );
 

Systolic _Array로 흘러가 Weight 계산일 하게됩니다

    pe pe0(.clk(clk), .rst_n(rst_n), .pixel_data(Pix_da0), .weight_data(Wei_da), .weight_valid(Weight_wr_en[0]), .back_accum(32'b0), .acc_result(pe_result[0]));
    pe pe1(.clk(clk), .rst_n(rst_n), .pixel_data(Pix_da0), .weight_data(Wei_da), .weight_valid(Weight_wr_en[1]), .back_accum(pe_result[0]), .acc_result(pe_result[1]));
    pe pe2(.clk(clk), .rst_n(rst_n), .pixel_data(Pix_da0), .weight_data(Wei_da), .weight_valid(Weight_wr_en[2]), .back_accum(pe_result[1]), .acc_result(pe_result[2]));
    pe pe3(.clk(clk), .rst_n(rst_n), .pixel_data(Pix_da1), .weight_data(Wei_da), .weight_valid(Weight_wr_en[3]), .back_accum(32'b0), .acc_result(pe_result[3]));
    pe pe4(.clk(clk), .rst_n(rst_n), .pixel_data(Pix_da1), .weight_data(Wei_da), .weight_valid(Weight_wr_en[4]), .back_accum(pe_result[3]), .acc_result(pe_result[4]));
    pe pe5(.clk(clk), .rst_n(rst_n), .pixel_data(Pix_da1), .weight_data(Wei_da), .weight_valid(Weight_wr_en[5]), .back_accum(pe_result[4]), .acc_result(pe_result[5]));
    pe pe6(.clk(clk), .rst_n(rst_n), .pixel_data(Pix_da2), .weight_data(Wei_da), .weight_valid(Weight_wr_en[6]), .back_accum(32'b0), .acc_result(pe_result[6]));
    pe pe7(.clk(clk), .rst_n(rst_n), .pixel_data(Pix_da2), .weight_data(Wei_da), .weight_valid(Weight_wr_en[7]), .back_accum(pe_result[6]), .acc_result(pe_result[7]));
    pe pe8(.clk(clk), .rst_n(rst_n), .pixel_data(Pix_da2), .weight_data(Wei_da), .weight_valid(Weight_wr_en[8]), .back_accum(pe_result[7]), .acc_result(pe_result[8]));
 

이렇게 pe에 선언이 되어있는데, 뭐야 왜 Weight가 다 똑같이 연결되어있어 할수 있지만

    generate
    for(i=0;i<9;i=i+1) begin
        assign Weight_wr_en[i] = (cnt==i) ? weight_valid : 0 ;
    end
    endgenerate
 

카운터 마다 weight_valid를 켜줘

Weight값이 PE에 차례로 들어가게 됩니다. 이로써 하나의 Systolic Array에 Weight를 담아, Conv 연산에 필요한 Weight가 저장되는것이죠 

    generate
    for(i=0;i<16;i=i+1) begin : Systolic
        Systolic_Array Systolic_Array(.clk(clk), .rst_n(rst_n), .Pix_da0(fifo_out[1]), .Pix_da1(fifo_out[0]), .Pix_da2(Pix_da),
         .Wei_da(Weight_da[DW*(16-i)-1:DW*(15-i)]), .weight_valid(weight_valid), .Fmap_da(Fmap_da[i]));
    end
    endgenerate
 

Channel Array를 보게되면 generate문으로 인해 각자 다른 Channel의 Weight값이 인가되도록합니다

여기까지 Weight가 Conv0_Layer에 어떻게 저장되는것인지 알아보았고 이후 Fmap이 어떻게 인가되는지 알아보겠습니다 


Fmap의 동작

먼저 간단한 예시를 들어 설명하겠습니다

Input Featuremap이 6*6 kernelsize가 3*3 stride 1 padding이 0이라고 하면

이와같이 되어있는 Featuremap일겁니다

이렇게 한 Conv slice(여기선 6)이 되겠죠

 

 

이렇게 PE에 저장되어있는 Weight가 있는데 Pix_da로값이 들어가게됩니다 그렇게되면 

최상단 FIFO1 중간 FIFO0 아래 Pixda

이렇게 계산이 될겁니다. 2차원이 아니라 Ram은 1차원으로 생각하면되므로 이렇게 쭈우욱 계산되게 되는데 

2차원으로생각하면 

이렇게 계산과정이 흘러가는 형태라고 볼 수 있습니다 


Fmap역시 Conv0_Inbuff에 저장되어있는 Fmap Data를 받아 Pix data로 Channel array에 들어오게됩니다 

이전에 포스팅했던것처럼 Fmap은 행단위로 저장되게 됩니다

Fmap data는 Conv_Inbuff에서 Pix_da_in으로 출력되게 되는데 이는 한 픽셀당 값을 의미합니다  한픽셀당 16비트의 데이터가 인가되어있는데,

    FIFO FIFO0 (.clk(clk), .rst_n(rst_n), .d_in(Pix_da), .wr_en(fifo_wr_en[0]), .rd_en(fifo_rd_en[0]), .d_out(fifo_out[0]));
    FIFO FIFO1 (.clk(clk), .rst_n(rst_n), .d_in(fifo_out[0]), .wr_en(fifo_wr_en[1]), .rd_en(fifo_rd_en[1]), .d_out(fifo_out[1]));
 

이처럼 FIFO에 의해 처음 Pix_da가 FiFO에 들어가 다음 FIFO로 들어가게되어,결국

첫번째 행이 FIFO1에 두번째행이 FIFO에 세번째 행이 Systolic Array에 들어가게됩니다

    generate
    for(i=0;i<16;i=i+1) begin : Systolic
        Systolic_Array Systolic_Array(.clk(clk), .rst_n(rst_n), .Pix_da0(fifo_out[1]), .Pix_da1(fifo_out[0]), .Pix_da2(Pix_da),
         .Wei_da(Weight_da[DW*(16-i)-1:DW*(15-i)]), .weight_valid(weight_valid), .Fmap_da(Fmap_da[i]));
    end
    endgenerate

 

Featuremap을 읽으려면 FIFO에 먼저 쓰고 읽어야겠죠

Current_row는 현재 행을 나타내고 Read_cnt는 현재 읽고있는 열을 나타냅니다.

Ram을 읽을때는 행으로 읽는다고했으니, 현재 행에 해당하는 모든 열의 Data를 다 읽고 다음 행을 읽도록 진행합니다. 따라서  Wave form을 보면 

첫번째 CurrentRow를 읽을때 첫번째 행은 Padding 처리 되어있기때문에 Fmap_rd_en을 꺼 Fmap의 data 대신 zero padding 즉 0의 값을 읽도록합니다

따라서 FIFO를 보게되면 Padding때 waddr가 증가하며 해당 행의 data가 읽히고 있는것이 보여지죠

이는 첫번째 행의 data를 FIFO1에 작성한것입니다.

FIFO2는 FIFO1의 data를 받아 첫번째 행의 data를 저장하고 FIFO1은 다음행의 data를 받아 두번째 행의 data를 받게되겠죠

    always @ (posedge clk or negedge rst_n)
    begin
        if(!rst_n) begin
            fifo_wr_en<=0;
        end
        else begin
            if(state==CONV) begin
                if(cnt>=0 && cnt<whole_read_cycle) begin
                    if(current_row==0) begin
                        fifo_wr_en[0]<=Fmap_rd_en||padding;
                        fifo_wr_en[1]<=0;
                    end
                    else if(current_row==1) begin
                        fifo_wr_en[0]<=Fmap_rd_en||padding;
                        fifo_wr_en[1]<=fifo_rd_en[0];
                    end
                    else if(current_row==slice_size_reg+1) begin
                        fifo_wr_en[0]<=0;
                        fifo_wr_en[1]<=0;
                    end
                    else begin
                        fifo_wr_en[0]<=1;
                        fifo_wr_en[1]<=1;
                    end
                end
                else begin
                    fifo_wr_en<=0;
                end
            end
            else begin
                fifo_wr_en<=0;
            end
        end
    end
 

이처럼 첫번째행을읽을때는 wr_en[1],즉 FIFO2의 write enable을 꺼줘 FIFO1에만 data가 읽히게두고

두번째행을 읽을때는 wr_en을 모두 켜줘 FIFO 1, 2 에 data가 모두 적힐수있도록합니다

이와같이 Flow가 구성되는데 실제 Conv 계산이되는 systolic Array를 보면

    FIFO FIFO0 (.clk(clk), .rst_n(rst_n), .d_in(Pix_da), .wr_en(fifo_wr_en[0]), .rd_en(fifo_rd_en[0]), .d_out(fifo_out[0]));
    FIFO FIFO1 (.clk(clk), .rst_n(rst_n), .d_in(fifo_out[0]), .wr_en(fifo_wr_en[1]), .rd_en(fifo_rd_en[1]), .d_out(fifo_out[1]));
   
    genvar i;
   
    generate
    for(i=0;i<16;i=i+1) begin : Systolic
        Systolic_Array Systolic_Array(.clk(clk), .rst_n(rst_n), .Pix_da0(fifo_out[1]), .Pix_da1(fifo_out[0]), .Pix_da2(Pix_da),
         .Wei_da(Weight_da[DW*(16-i)-1:DW*(15-i)]), .weight_valid(weight_valid), .Fmap_da(Fmap_da[i]));
    end
    endgenerate
 

이처럼 구성되어있습니다. 즉 Pix_da0가 첫행 Pix_da1이 두번째행 Pix_da 2가 세번째행이게되는데

Fifo_out이 모든 행의 data를 보내려면 slice_size+1만큼 기다려야 한 행의 data가 온전히 나오게됩니다

따라서 FIFO1 첫번째 행의 data 즉 padding의 data가 나오면 FIFO0에서는 두번째행의 data가 나오고 그때 Pixda2는 세번째행의 data를 내보내게됩니다

이처럼 구성되게 되는데 실제 Conv가 일어날때는  하얀색 박스일때므로 Pix_da0에 첫번째행 Pix_da1에는 두번째행 Pix_da2에는 세번째 행의 data가 있도록 되어있습니다

그결과 Fmap data가 되게되는데 

    assign Fmap_da_out = {Fmap_da[0],Fmap_da[1],Fmap_da[2],Fmap_da[3],Fmap_da[4],Fmap_da[5],Fmap_da[6],Fmap_da[7],
                          Fmap_da[8],Fmap_da[9],Fmap_da[10],Fmap_da[11],Fmap_da[12],Fmap_da[13],Fmap_da[14],Fmap_da[15]};
 

이와같이 선언을 하여 Channel에 대해서 나타내주고 Fmap들이 

    generate
    for(j=0;j<Out_ch;j=j+1) begin : Conv_out
        assign Conv_data[j] = Fmap_da_out[DW*(16-j)-1:DW*(15-j)];
    end
    endgenerate

이와같이 모이게되어  Conv_data가 되도록합니다 그것이 Conv_mem의 메모리에 들어가게되는데

//Conv_control   
always @ (posedge clk or negedge rst_n)
    begin
        if(!rst_n) begin
            Conv_out_valid<=0;
            conv_cnt<=0;
        end
        else begin
            if(state==CONV)begin
                if(cnt>=(slice_size+2)*2+5) begin
                    if(conv_cnt==slice_size_reg+1)
                        conv_cnt<=0;
                    else
                        conv_cnt<=conv_cnt+1;
                    if(conv_cnt>=0 && conv_cnt<slice_size_reg)
                        Conv_out_valid<=1;
                    else
                        Conv_out_valid<=0;
                end
                else
                    Conv_out_valid<=0;  
            end
            else begin
                conv_cnt<=0;
                Conv_out_valid<=0;
            end
        end
    end
 

Conv_control에 따라  Conv_out_valid를 받게되면 Conv_mem의 wr_en이 on이되어 Fmap의 data가 쓰여지게됩니다. 여기서 쓰여진 Fmap의 data는 Pooliing 및 Activation에서 필요하게되는데 그것은 차후에 다루도록 하겠습니다.

따라서 결과적으로 Convolution 결과들이 모여

    assign Cram_da_out_bundle = {Cram_da_out[0], Cram_da_out[1], Cram_da_out[2], Cram_da_out[3],
                                  Cram_da_out[4], Cram_da_out[5], Cram_da_out[6], Cram_da_out[7],
                                  Cram_da_out[8], Cram_da_out[9], Cram_da_out[10], Cram_da_out[11],
                                  Cram_da_out[12], Cram_da_out[13], Cram_da_out[14], Cram_da_out[15]};

번들로 구성되어 다음 PostConvlayer에서 이용되도록합니다


+

Maxpooling 시 Cram에서 읽는방법

cram에서 data를 읽을때 저장은 

이처럼 한 행씩 저장하게 됩니다

하지만 Maxpooling은 

이와같이 Maxpooling에 이루어져야하는데 어떻게 해야 그렇게 할수있을까요? 바로 읽는방법에서 차이가 있습니다

    reg [1:0] Cram_rd_cnt;
    reg [3:0] Cram_row_cnt;
    always @(posedge clk or negedge rst_n)
    begin
        if(!rst_n)
        begin
            Cram_raddr <= 0;
            Cram_rd_cnt <= 0;
            Cram_row_cnt <= 0;
        end
        else
        begin
            if(Cram_rd_en)
            begin
                if(Cram_raddr == Conv0_slice**2 - 1)
                begin
                    Cram_raddr <= 0;
                end
                else
                begin
                    if(Cram_rd_cnt == 3)
                    begin
                        if(Cram_row_cnt == 13)
                        begin
                            Cram_row_cnt <= 0;
                            Cram_raddr <= Cram_raddr + 1;
                        end
                        else
                        begin
                            Cram_raddr <= Cram_raddr - Conv0_slice + 1;
                            Cram_row_cnt <= Cram_row_cnt + 1;
                        end
                       
                    end
                    else if(Cram_rd_cnt == 1)
                    begin
                        Cram_raddr <= Cram_raddr + Conv0_slice - 1;
                    end
                    else
                    begin
                        Cram_raddr <= Cram_raddr + 1;
                    end
                end
                Cram_rd_cnt <= Cram_rd_cnt + 1;
            end
            else
            begin
                Cram_raddr <= 0;
                Cram_rd_cnt <= 0;
                Cram_row_cnt <= 0;
            end
        end
    end
 

처음 이걸 보면 무슨말 하는지 모를수 있습니다 다시 정리해보면

이와같이 정리할수있습니다. 이걸봐도 모를수도 있습니다

그림으로 설명하면

이와같이 읽기동작이 나타나게됩니다 

rd_cnt는 열을 읽는동작이고 Row_cnt는 행을 읽는 동작입니다

그렇게되면 이와같이 동작한다는것을 볼 수 있습니다