[Mini-NPU RTL] Design 1D PE Chain

"1D PE Chain"

pe_mac.v

Design Processing Element의 pe_mac.v와 동일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
`timescale 1ns / 1ps
module pe_mac #(
    parameter data_width = 8,               // 데이터 크기 = 8-bit
    parameter mul_width = 2 * data_width,   // Multiplication (곱한 값은 최대 2 * data_width)
    parameter acc_width = 2 * mul_width     // Accumulation (곱한 결과 값 누적은 최대 2 * mul_width)
) (
    input clk,
    input rst,
    input clear,                             // Accumulator 초기화
    input en,                                // 연산 가능 신호
    input [data_width-1:0] a_in,
    input [data_width-1:0] b_in,
    output [mul_width-1:0] mul_out,
    output reg [acc_width-1:0] acc_sum_out
);
 
    assign mul_out = a_in* b_in;             // 곱셈 연산
 
    always @ (posedge clk or negedge rst) begin
        if (!rst) begin                      // reset 신호
            acc_sum_out <= 0;                // Accumulator 초기화
        end else if (clear) begin            // Accumulator clear 신호
            acc_sum_out <= 0;                // Accumulator 초기화
        end else if (en) begin               // 누적 연산 가능 신호
            acc_sum_out <= acc_sum_out + mul_out;   // 누적 연산
        end
    end
endmodule
 
cs

pe_chain_1d.v

"pe_mac.v" 코드를 Instance
4개의 PE를 선언하여 입력 데이터 원본이 PE0 → PE1 → PE2 → PE3 순서로 Clock이 뛸 때마다 한 칸씩 이동
"generate" 구문을 사용하기 위해 모든 Type을 Verilog → System Verilog로 변경

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
`timescale 1ns / 1ps
module pe_chain_1d # (
    parameter data_width = 8,                         // 데이터 크기 = 8-bit
    parameter mul_width = 2 * data_width,             // Multiplication (곱한 값은 최대 2 * data_width)
    parameter acc_width = 2 * mul_width,              // Accumulation (곱한 결과 값 누적은 최대 2 * mul_width)
    parameter num_pe = 4
) (
    input clk,
    input rst,
    input clear,                                        // Accumulator 초기화
    input en,                                           // 연산 가능 신호
    input [data_width-1:0] data_in,
    input [data_width-1:0] weight_in [0:num_pe-1],      // 8-bit 크기의 Weight 4개
    output [data_width-1:0] data_out,
    output [mul_width-1:0] pe_mul_out [0:num_pe-1],     // 16-bit 크기의 곱셈 값 4개
    output [acc_width-1:0] pe_acc_sum_out [0:num_pe-1]  // 32-bit 크기의 누적 값 4개
);
 
    // 데이터 pipeline (data_pipe[0= data_in, data_pipe[3= data_out)
    reg [data_width-1:0] data_pipe [0:num_pe-1];
    assign data_out = data_pipe[num_pe-1];              // 만약 1x8이면 data_out이 두 번째 세트의 data_in
 
    // 데이터 pipeline register
    generate
    for (genvar i = 0; i < num_pe; i++begin
        if (i == 0begin                                   // 맨 처음 PE인 경우
            always @ (posedge clk or negedge rst) begin
                if (!rst) begin                             // reset인 경우
                    data_pipe[i] <= 0;                      // 초기화
                end else begin                              // reset이 아닌 경우
                    data_pipe[i] <= data_in;                // 입력 데이터를 저장
                end
            end
        end else begin                                      // 맨 처음 PE가 아닌 경우
            always @ (posedge clk or negedge rst) begin
                if (!rst) begin                             // reset인 경우
                    data_pipe[i] <= 0;                      // 초기화
                end else begin                              // reset이 아닌 경우
                    data_pipe[i] <= data_pipe[i-1];         // 이전 PE에 저장된 입력 데이터를 저장
                end
            end
        end
    end
    endgenerate
 
    // 각 stage에 맞춰 pe_mac instance
    generate
    for (genvar i = 0; i < num_pe; i++begin
        pe_mac # (
            .data_width(data_width),
            .mul_width(mul_width),
            .acc_width(acc_width)
        ) unit_pe_mac (
            .clk(clk),
            .rst(rst),
            .clear(clear),
            .en(en),
            .a_in(data_pipe[i]),
            .b_in(weight_in[i]),
            .mul_out(pe_mul_out[i]),
            .acc_sum_out(pe_acc_sum_out[i])
        );
    end
    endgenerate
endmodule
 
cs

tb_pe_chain_1d.v

"pe_chain_1d.v" 코드를 Instance
4개의 Stage 단계 일치시키기 중요
테스트 벤치 코드에서도 pe_mac과 pe_chain_1d를 구현하여 DUT에서 계산한 결과와 TB에서 계산한 결과가 동일한지 확인
서로 맞지 않다면 Count를 늘려가며 통계에 사용
입력 값 "data_in"은 0~255 범위 중 랜덤한 값을 사용
Weight 값은 [1, 2, 3, 4]로 고정하여 Weight Stationary 구조 사용
누적 신호 값 "en"은 0/1 중 랜덤한 값을 사용, "1"일 때 누적 진행
누적 값 초기화 "clear"는 5% 확률로 "1"이 되어 초기화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
`timescale 1ns / 1ps
module tb_pe_chain_1d;
    localparam data_width = 8;              // TB 파일에서만 적용
    localparam mul_width = 2 * data_width;  // TB 파일에서만 적용
    localparam acc_width = 2 * mul_width;   // TB 파일에서만 적용
    localparam num_pe = 4;                  // TB 파일에서만 적용
    
    reg clk;
    reg rst;
    reg clear;
    reg en;
    reg [data_width-1:0] data_in;
    reg [data_width-1:0] weight_in [0:num_pe-1];
    
    wire [data_width-1:0] data_out;
    wire [mul_width-1:0] pe_mul_out [0:num_pe-1];
    wire [acc_width-1:0] pe_acc_sum_out [0:num_pe-1];
    
    // 통계를 위한 Count 변수
    integer err_mul_cnt;
    integer err_acc_cnt;
    integer total_cnt;
//==================================================================
// 1) Instance를 통한 port 연결
    pe_chain_1d # (                 // TB 파일에서 설정한 값을 DUT에도 적용하기 위함
        .data_width(data_width),
        .mul_width(mul_width),
        .acc_width(acc_width),
        .num_pe(num_pe)
    ) dut (                         // DUT 이름으로 Instance
        .clk(clk),
        .rst(rst),
        .clear(clear),
        .en(en),
        .data_in(data_in),
        .weight_in(weight_in),
        .data_out(data_out),
        .pe_mul_out(pe_mul_out),
        .pe_acc_sum_out(pe_acc_sum_out)
    );
//==================================================================
// 2) Clock & Reset
    initial begin
        clk <= 0;
        forever #5 clk <= ~clk;     // 주기 10ns
    end
    
    initial begin
        rst <= 0;                   // 초기화
        clear <= 0;                 // 초기화
        en <= 0;                    // 초기화
        data_in = 0;                // 초기화
        
        // Weight를 랜덤으로 받지 않고 123, 4로 고정
        for (int i = 0; i < num_pe; i++begin
            weight_in[i] = i + 1;
        end
 
        #30                         // 30ns 이후
        rst <= 1;                   // reset 신호 인가
        
        #3000                       // 3000ns 동안 랜덤 입력과 고정 Weight에 대한 계산 진행
        // 통계 출력
        $display("===========================");
        $display("[TB] 1D PE Chain Simulation");
        $display("Simulation Time : %0d ns", $time);
        $display("[Total Cycles] : %d", total_cnt);
        $display("[MUL Errors]    : %d", err_mul_cnt);
        $display("[ACC Errors]    : %d", err_acc_cnt);
        $finish;                    // 종료
    end
//==================================================================
// 3) DUT와 TB의 데이터 Pipeline값을 비교하기 위해 DUT와 똑같은 Logic을 TB 내부에 구현
    // TB용 데이터 pipeline register
    reg [data_width-1:0] tb_data_pipe [0:num_pe-1];
    
    generate
    for (genvar i = 0; i < num_pe; i++begin
        if (i == 0begin                                   // 맨 처음 PE인 경우
            always @ (posedge clk or negedge rst) begin
                if (!rst) begin                             // reset인 경우
                    tb_data_pipe[0<= 0;                   // 초기화
                end else begin                              // reset이 아닌 경우
                    tb_data_pipe[0<= data_in;             // 입력 데이터를 저장
                end
            end
        end else begin                                      // 맨 처음 PE가 아닌 경우
            always @ (posedge clk or negedge rst) begin
                if (!rst) begin                             // reset인 경우
                    tb_data_pipe[i] <= 0;                   // 초기화
                end else begin                              // reset이 아닌 경우
                    tb_data_pipe[i] <= tb_data_pipe[i-1];   // 이전 PE에 저장된 입력 데이터를 저장
                end
            end
        end
    end
    endgenerate
//==================================================================
// 4) DUT와 TB의 mul값과 누적 값을 비교하기 위해 DUT와 똑같은 Logic을 TB 내부에 구현
    wire [mul_width-1:0] tb_mul_out [0:num_pe-1];
    reg [acc_width-1:0] tb_acc_sum_out [0:num_pe-1];
    
    generate
    for (genvar i = 0; i < num_pe; i++begin
        assign tb_mul_out[i] = tb_data_pipe[i] * weight_in[i];  // 곱셈 계산
    end
    endgenerate
 
    always @ (posedge clk or negedge rst) begin
        if (!rst) begin                                 // reset 신호
            for (int i = 0; i < num_pe; i++begin
                tb_acc_sum_out[i] <= 0;                 // Accumulator 초기화
            end
        end else if (clear) begin                       // clear 신호
            for (int i = 0; i < num_pe; i++begin
                tb_acc_sum_out[i] <= 0;                 // Accumulator 초기화
            end
        end else if (en) begin                          // 누적 연산 가능 신호
            for (int i = 0; i < num_pe; i++begin
                tb_acc_sum_out[i] <= tb_acc_sum_out[i] + tb_mul_out[i]; // 누적 연산
            end
        end
    end
//==================================================================
// 5) 매 clock마다 DUT의 결과와 TB 결과를 비교
    always @ (posedge clk) begin
        if (!rst) begin
            err_mul_cnt <= 0;
            err_acc_cnt <= 0;
            total_cnt <= 0;
        end else begin
            total_cnt <= total_cnt + 1;
            for (int i = 0; i < num_pe; i++begin
                if (pe_mul_out[i] !== tb_mul_out[i]) begin
                    $display("[PE%0d MUL ERROR] Time=%0t | DUT=%d, TB=%d", i, $time, pe_mul_out[i], tb_mul_out[i]);
                    err_mul_cnt <= err_mul_cnt + 1;
                end else begin
                    $display("[PE%0d MUL PASS] Time=%0t | DUT=%d, TB=%d", i, $time, pe_mul_out[i], tb_mul_out[i]);
                end
                if (pe_acc_sum_out[i] !== tb_acc_sum_out[i]) begin
                    $display("[PE%0d ACC ERROR] Time=%0t | DUT=%d, TB=%d", i, $time, pe_acc_sum_out[i], tb_acc_sum_out[i]);
                    err_acc_cnt <= err_acc_cnt + 1;
                end else begin
                    $display("[PE%0d ACC PASS] Time=%0t | DUT=%d, TB=%d", i, $time, pe_acc_sum_out[i], tb_acc_sum_out[i]);
                end
            end
        end
    end
//==================================================================
// 6) 입력 생성
    always @ (posedge clk or negedge rst) begin
        if (!rst) begin
            data_in <= 0;
        end else begin
            data_in <= $urandom_range(0, (1<<data_width)-1);    // 1을 왼쪽으로 8칸 shift = 256(1_0000_0000)
            en <= $urandom_range(01);                         // 0/1 중 랜덤, 1일 때 mul 값을 누적
            if ($urandom_range(099< 5begin                // 5% 확률로 clear 신호 켜서 acc 값 초기화
                clear <= 1;
            end else begin
                clear <= 0;
            end
        end
    end
endmodule
 
cs

Result

"#30 rst <= 1"에 의해 30ns 이후 clk 신호부터 값이 들어오는 것을 확인 가능
Weight Stationary 방식에 맞게 Weight는 [1, 2, 3, 4]로 고정되어 있는 것을 확인 가능
data_pipe가 Cycle마다 입력 데이터에 따라 쌓이면서 변하는 것을 확인 가능
data_out은 4Cycle 후부터 출력되는 것을 확인 가능
55~85ns에 "en <= 1"에 의해 acc_sum_out에 곱셈 결과가 누적되는 것을 확인 가능
각 PE에 따라 누적되는 값이 다른 것을 확인 가능

clear 신호로 인해 4개의 PE의 누적 값이 초기화 되고, 345ns의 "en <= 1" 신호에 의해 다시 누적을 시작하는 것을 확인 가능

"#30 rst <= 1"에 의해 3030ns에 종료되어 최종 통계가 출력되는 것을 확인 가능

Reference

▶ Design Processing Element
https://hecess.blogspot.com/2026/01/minit-npu-rtl-design-processing-element.html

▶ Design 2D Systolic Array
https://hecess.blogspot.com/2026/01/mini-npu-rtl-design-2d-systolic-array.html

▶ Design Systolic Array + Controller
https://hecess.blogspot.com/2026/01/mini-npu-rtl-design-systolic-array.html

▶ Design Systolic Array + Controller + Activation(ReLU)
https://hecess.blogspot.com/2026/01/mini-npu-rtl-design-systolic-array_27.html

댓글

이 블로그의 인기 게시물

[Mini-NPU RTL] NN Reference Model

[Mini-NPU RTL] TPU (Study Paper)