[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(0, 1); // 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(0, 0) 데이터 할당 a_in_row[1] <= 0; // 1번째 행 대기 b_in_col[0] <= B[0][0]; // 0번째 열에 B(0, 0) 데이터 할당 b_in_col[1] <= 0; // 1번째 열 대기 // Cycle 1 @ (posedge clk); a_in_row[0] <= A[0][1]; // 0번째 행에 A(0, 1) 데이터 할당 a_in_row[1] <= A[1][0]; // 1번째 행에 A(1, 0) 데이터 할당 b_in_col[0] <= B[1][0]; // 0번째 열에 B(1, 0) 데이터 할당 b_in_col[1] <= B[0][1]; // 1번째 열에 B(0, 1) 데이터 할당 // Cycle 2 @ (posedge clk); a_in_row[0] <= 0; // 0번째 행 데이터 입력 끝 a_in_row[1] <= A[1][1]; // 1번째 행에 A(1, 1) 데이터 할당 b_in_col[0] <= 0; // 0번째 열 데이터 입력 끝 b_in_col[1] <= B[1][1]; // 1번째 열에 B(1, 1) 데이터 할당 // 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
"#10 en <= 1"에 의해 40ns에 en이 1로 올라가는 것을 확인 가능
"a_in_row"와 "b_in_col"이 첫 Cycle에는 첫 번째에만 데이터가 할당
→ 이후 Clock이 넘어가면서 데이터도 같이 넘어가는 것을 확인 가능
곱셈이 모두 진행된 후 "pe_acc_sum_out" 결과와 SW 결과인 "C"의 값이 같아 Console 창에 PASS가 출력
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
댓글
댓글 쓰기