[Mini-NPU RTL] Design 2D Systolic Array

"2D Systolic Array"

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_systolic_cell.v

하나의 PE가 데이터를 받은 후, 계산하고 출력하는 동작 구현
"en" 신호가 누적 값을 제어하는 것이 아니라, en 신호에 맞춰 register를 데이터 값으로 업데이트
"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
`timescale 1ns / 1ps
module pe_systolic_cell # (
    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,
    input en,
    
    input [data_width-1:0] a_in,
    input [data_width-1:0] b_in,
    output [data_width-1:0] a_out,          // 전달되는 값
    output [data_width-1:0] b_out,          // 전달되는 값
    output [mul_width-1:0] mul_out,
    output reg [acc_width-1:0] acc_sum_out
);
 
    // a, b 값을 한 번 register에 저장 후 옆/아래로 전달
    reg [data_width-1:0] a_reg, b_reg;
    assign a_out = a_reg;
    assign b_out = b_reg;
    
    always @ (posedge clk or negedge rst) begin
        if (!rst) begin             // reset 신호
            a_reg <= 0;             // reg값 초기화
            b_reg <= 0;             // reg값 초기화
        end else if (en) begin      // en 신호
            a_reg <= a_in;          // reg값 update
            b_reg <= b_in;          // reg값 update
        end
    end
    
    // 내부 MAC 연산 동작은 pe_mac Instance를 통해 수행
    pe_mac # (                      // 이 파일에서 설정한 값을 pe_mac 파일에도 적용하기 위함
        .data_width(data_width),
        .mul_width(mul_width),
        .acc_width(acc_width)
    ) unit_pe_mac (                 // unit_pe_mac 이름으로 Instance
        .clk(clk),
        .rst(rst),
        .clear(clear),
        .en(en),
        .a_in(a_reg),               // a_reg * b_reg 누산
        .b_in(b_reg),               // a_reg * b_reg 누산
        .mul_out(mul_out),
        .acc_sum_out(acc_sum_out)
    );
endmodule
 
cs

systolic_array_2d.v

각 PE를 구조적으로 연결하여, PE에서 받은 데이터를 다음 PE로 넘기는 전체 흐름을 구현

Rows x Columns 크기의 Grid 모양으로 pe_systolic_cell을 배치

→ c=0인 경우 a_temp[r][0]을 받고, r=0인 경우 b_temp[0][c]을 받음

→ a_temp{r]{c]를 입력으로 받아 데이터를 사용한 후, a_temp{r][c+1]로 넘김

→ b_temp[r][c]를 입력으로 받아 데이터를 사용한 후, b_temp[r+1][c]로 넘김

"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
`timescale 1ns / 1ps
module systolic_array_2d # (
    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 rows = 2,                     // 행 개수
    parameter cols = 2                      // 열 개수
) (
    input clk,
    input rst,
    input clear,
    input en,
    input [data_width-1:0] a_in_row [0:rows-1],                   // 왼쪽에서 들어오는 A 행렬 stream (각 행마다 입력 하나)
    input [data_width-1:0] b_in_col [0:cols-1],                   // 위쪽에서 들어오는 B 행렬 stream (각 열마다 입력 하나)
    output [mul_width-1:0] pe_mul_out [0:rows-1][0:cols-1],       // 각 PE의 곱셈 결과
    output [acc_width-1:0] pe_acc_sum_out [0:rows-1][0:cols-1]    // 각 PE의 누적 결과
);
 
    wire [data_width-1:0] a_temp [0:rows-1][0:cols];        // 2개의 열을 외부 입력 - 연결 - 외부 출력 연결
    wire [data_width-1:0] b_temp [0:rows][0:cols-1];        // 2개의 행을 외부 입력 - 연결 - 외부 출력 연결
    
    genvar r, c;                                // 반복문에 사용할 변수 선언
    
    generate
    for (r = 0; r < rows; r++begin            // 행 반복
        assign a_temp[r][0= a_in_row[r];      // 각 행에 맞는 데이터를 0번째 열에 공급
    end
    for (c = 0; c < cols; c++begin            // 열 반복
        assign b_temp[0][c] = b_in_col[c];      // 각 열에 맞는 데이터를 0번째 행에 공급
    end
    endgenerate
    
    generate
    for (r = 0; r < rows; r++begin           // 행 반복
        for (c = 0; c < cols; c++begin       // 열 반복
            pe_systolic_cell # (               // 이 파일에서 설정한 값을 pe_systolic_cell 파일에도 적용하기 위함
                .data_width(data_width),
                .mul_width(mul_width),
                .acc_width(acc_width)
            ) unit_cell (                       // unit_cell 이름으로 Instance
                .clk(clk),
                .rst(rst),
                .clear(clear),
                .en(en),
                .a_in(a_temp[r][c]),            // 현재 위치에서 입력을 받음
                .a_out(a_temp[r][c+1]),         // 다음 위치로 출력을 보냄
                .b_in(b_temp[r][c]),            // 현재 위치에서 입력을 받음
                .b_out(b_temp[r+1][c]),         // 다음 위치로 출력을 보냄
                .mul(pe_mul_out[r][c]),         // 곱셈 결과
                .acc_sum(pe_acc_sum_out[r][c])  // 누적 결과
            );
        end
    end
    endgenerate
endmodule
 
cs

tb_systolic_array_2d.v

SW로 행렬 곱을 계산한 결과와 Systolic으로 계산한 결과를 비교

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
`timescale 1ns / 1ps
module tb_systolic_array_2d;
    localparam k_dim = 2;                   // A, B의 공통 차원(누산 시 발생할 수 있는 Overflow 방지)
    localparam data_width = 8;              // 데이터 크기 = 8-bit
    localparam mul_width = 2 * data_width;  // Multiplication (곱한 값은 최대 2 * data_width)
    localparam acc_width = 2 * mul_width;   // Accumulation (곱한 결과 값 누적은 최대 2 * mul_width)
    localparam rows = 2;                    // 행 개수
    localparam cols = 2;                    // 열 개수
    localparam total_cycles = 10;           // MAC 연산 반복 횟수
 
    reg clk;
    reg rst;
    reg clear;
    reg en;
    reg [data_width-1:0] a_in_row [0:rows-1];
    reg [data_width-1:0] b_in_col [0:cols-1];
    wire [mul_width-1:0] pe_mul_out [0:rows-1][0:cols-1];
    wire [acc_width-1:0] pe_acc_sum_out [0:rows-1][0:cols-1];
 
    // Instance로 port 연결
    systolic_array_2d # (                   // 이 파일에서 설정한 값을 systolic_array_2d 파일에도 적용하기 위함
        .data_width(data_width),
        .mul_width(mul_width),
        .acc_width(acc_width),
        .rows(rows),
        .cols(cols)
    ) dut (                                 // dut 이름으로 Instance
        .clk(clk),
        .rst(rst),
        .clear(clear),
        .en(en),
        .a_in_row(a_in_row),
        .b_in_col(b_in_col),
        .pe_mul_out(pe_mul_out),
        .pe_acc_sum_out(pe_acc_sum_out)
    );
    
    int A [0:rows-1][0:k_dim-1];    // SW에서 계산할 A행렬 선언
    int B [0:k_dim-1][0:cols-1];    // SW에서 계산할 B행렬 선언
    int C [0:rows-1][0:cols-1];     // SW에서 행렬곱 결과를 저장할 C 행렬선언
    int err_cnt;                    // 통계를 위한 변수
    
    initial begin
        clk <= 0;
        forever #5 clk <= ~clk;     // 주기 10ns
    end
    
    initial begin
        rst <= 0;                    // 초기화
        en <= 0;                     // 초기화
        a_in_row <= '{default:0};    // 배열 일괄 초기화
        b_in_col <= '{default:0};    // 배열 일괄 초기화
        
        #30                          // 30ns 이후
        rst <= 1;                    // 동작 신호
        #10                          // 10ns 이후
        en <= 1;                     // 동작 신호
 
        for (int iter = 0; iter < total_cycles; iter++begin                   // 10회 반복
            en = $urandom_range(01);                                          // 0/1 중 랜덤, 1일 때 값을 받음
            foreach(A[r,c]) A[r][c] = $urandom_range(0, (1 << data_width)-1);   // 모든 행과 열에 랜덤 값 할당
            foreach(B[r,c]) B[r][c] = $urandom_range(0, (1 << data_width)-1);   // 모든 행과 열에 랜덤 값 할당
            
            // SW로 행렬곱 계산
            for (int r = 0; r < rows; r++begin             // 행 반복
                for (int c = 0; c < cols; c++begin         // 열 반복
                    C[r][c] = 0;                             // 정답 행렬 초기화
                    for (int k = 0; k < k_dim; k++begin
                        C[r][c] += A[r][k] * B[k][c];        // A의 r행 k번째 값과 B의 k행 c번째 값을 곱함
                    end
                end
            end
        
            // Systolic으로 계산
            // Cycle 0
            @ (posedge clk);
            a_in_row[0<= A[0][0];     // 0번째 행에 A(00) 데이터 할당
            a_in_row[1<= 0;           // 1번째 행 대기
            b_in_col[0<= B[0][0];     // 0번째 열에 B(00) 데이터 할당
            b_in_col[1<= 0;           // 1번째 열 대기
        
            // Cycle 1
            @ (posedge clk);
            a_in_row[0<= A[0][1];     // 0번째 행에 A(01) 데이터 할당
            a_in_row[1<= A[1][0];     // 1번째 행에 A(10) 데이터 할당
            b_in_col[0<= B[1][0];     // 0번째 열에 B(10) 데이터 할당
            b_in_col[1<= B[0][1];     // 1번째 열에 B(01) 데이터 할당
        
            // Cycle 2
            @ (posedge clk);
            a_in_row[0<= 0;           // 0번째 행 데이터 입력 끝
            a_in_row[1<= A[1][1];     // 1번째 행에 A(11) 데이터 할당
            b_in_col[0<= 0;           // 0번째 열 데이터 입력 끝
            b_in_col[1<= B[1][1];     // 1번째 열에 B(11) 데이터 할당
        
            // Cycle 3
            @ (posedge clk);
            a_in_row[1<= 0;           // 1번째 행 데이터 입력 끝
            b_in_col[1<= 0;           // 1번째 열 데이터 입력 끝
        
            // 연산 대기
            repeat(5) @ (posedge clk);  // 5 Clock 동안 대기
        
            // SW 계산 결과와 Systolic 계산 결과 비교
            err_cnt = 0;
            $display("======================================================");
            $display("[TB] Iteration %0d Checking C = A * B result", iter+1);
        
            if (en) begin
                for (int r = 0; r < rows; r++begin
                    for (int c = 0; c < cols; c++begin
                        if (pe_acc_sum_out[r][c] !== C[r][c]) begin
                            err_cnt++;
                            $display("ERROR! C[%0d][%0d] mismatch : DUT=%0d, TB=%0d, time=%0d", r, c, pe_acc_sum_out[r][c], C[r][c], $time);
                        end else begin
                            $display("PASS! C[%0d][%0d] match : DUT=%0d, TB=%0d, time=%0d", r, c, pe_acc_sum_out[r][c], C[r][c], $time);
                        end
                    end
                end
            
                if (err_cnt == 0)
                    $display("[TB] RESULT : PASS");
                else
                    $display("[TB] RESULT : FAIL (# of Error=%0d)", err_cnt);
                $display("======================================================");
            end
        
            // reset & 다음 loop 준비
            rst <= 0;                     // 초기화 실행
            repeat(2) @ (posedge clk);    // 확실하게 초기화 되도록 2 Cycle 동안 초기화 실행
            rst <= 1;                     // 다시 동작
            @ (posedge clk);              // 회로가 안정될 때까지 1 Cycle 대기
        end                               // End of Loop
    
        $display("===== 10 Random Tests Finished! =====");
        $finish;
    end
endmodule
 
cs

Result

"#30 rst <= 1"에 의해 30ns에 rst가 1로 올라가는 것을 확인 가능
"#10 en <= 1"에 의해 40ns에 en이 1로 올라가는 것을 확인 가능
"a_in_row"와 "b_in_col"이 첫 Cycle에는 첫 번째에만 데이터가 할당
→ 이후 Clock이 넘어가면서 데이터도 같이 넘어가는 것을 확인 가능
곱셈이 모두 진행된 후 "pe_acc_sum_out" 결과와 SW 결과인 "C"의 값이 같아 Console 창에 PASS가 출력

"en <= 0"인 경우, MAC 연산이 진행되지 않아 "pe_acc_sum_out"이 0이 되고, 이에 따라 Console 창은 해당 반복을 Skip

Thinking

· generate를 사용한 for 구문 : Simulation 실행 전에 실행되며, HW를 복사하는 목적으로 사용
· initial/always 구문 안에서 사용한 for 구문 : 시간 순서대로 실행되며, 값 변경을 목적으로 사용

Reference

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

▶ Design 1D PE Chain
https://hecess.blogspot.com/2026/01/mini-npu-rtl-design-1d-pe-chain.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)