[Embedded System Lab] Chapter 7 - 8x8 PE Implementation

"8x8 PE Implementation"

[Objective]

8x8 PE Array를 설계해서 MAC 연산 기반 행렬 곱셈 결과를 Simulation을 통해 검증하여 Logic 이해하기

[Theory]

▣ PE(Processing Element)
· 컴퓨터 System에서 사용되는 기본 연산 단위
· 핵심 기능은 MAC 연산
→ 많은 PE가 반복적으로 동작하면서 대규모 병렬 연산을 빠르게 수행 가능

<PE Structure>
· Wn(Weight)와 An(Input Activation)을 곱한 결과를 Yn-1(이전까지 누적된 Partial Sum)과 더함

▣ Stationary
· 데이터를 메모리에서 계속 가져오지 않고, 한 번 가져온 데이터를 PE 내부에 유지하여 재사용하는 방식
    (Output Stationary, Weight Stationary, Input Stationary, Row Stationary)
→ 시간도 많이 걸리고 전력 소모도 큰 메모리 접근 횟수를 감소하여 효율 증가

> Output Stationary
· 각 PE가 계산하는 출력 값(Partial Sum)을 누적하면서 제자리에서 유지
· Data Flow : Input Activation = 여러 번 흐름 / Weight = 여러 번 흐름 / Partial Sum = PE 내부에 유지

> Weight Stationary
· 각 PE가 필요한 Weight를 제자리에 유지하고 재사용
· Data Flow : Input Activation = 여러 번 흐름 / Weight = PE 내부에 유지 / Partial Sum = 생성되어 외부로 출력

<Weight Stationary 기반 PE Structure>

▣ Weight Preload Workflow
   
   
   
· 연산 전에 Weight를 먼저 넣어 연산 중에 Weight 이동을 줄이고, 입력 데이터와 Partial Sum 흐름에 집중

▣ Partial Sum Workflow
① Weight가 PE Array에 미리 저장
② Input 데이터가 Cycle마다 지연되어 흐르면서 각 PE에서 MAC 연산 수행
③ 출력 행렬의 원소가 누산

** Weight Stationary 방식이므로 Weight는 자리를 유지하고 Input이 흐르면서 출력 값을 계산

[Experiment]


  <Input Data>                                    <Weight Data>

▣ tb_pe_8x8.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
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
// System Verilog로 진행
`timescale 1ns / 1ps
module tb_pe_8x8;
    reg clk;                                           // Clock 신호
    reg rst;                                           // Reset 신호
    reg preload;                                       // Weight Preload 신호
    reg mac;                                           // MAC 연산 수행 신호
    reg signed[7:0] weight[0:7];                       // 8-bit Signed 값 8개
    reg signed[7:0] in[0:7];                           // 8-bit Signed 값 8개
    wire signed[17:0] out[0:7];                        // 18-bit Signed 값 8개
    
    integer r, c, k;
    
    reg signed[7:0] in_data[0:63];                     // txt 파일의 In Data를 Load (1차원 배열)
    reg signed[7:0] weight_data[0:63];                 // txt 파일의 Weight Data를 Load (1차원 배열)
    reg signed[17:0] amm[0:7][0:7];                    // Actual Matrix Multiplication 계산 (8x8)
    
    // pe_8x8 Module Instance
    pe_8x8 dut (
        .clk(clk),
        .rst(rst),
        .preload(preload),
        .mac(mac),
        .in(in),
        .weight(weight),
        .out(out)
    );
    
    initial begin
        clk <= 1;                                       // clk 초기화
        forever #1 clk <= ~clk;                         // 1ns마다 반전 = 주기 2ns
    end
    
    initial begin
        rst <= 1;                                       // Reset 신호 On
        preload <= 0;                                   // Preload 초기화
        mac <= 0;                                       // MAC 초기화
        $readmemh("input.txt", in_data);                // txt 파일에서 In 데이터 Load
        $readmemh("weight.txt", weight_data);           // txt 파일에서 Weight 데이터 Load
        
        #1 rst <= 0;                                    // Reset 신호 Off
        
        // 정답 계산
        for (c = 0; c < 8; c = c + 1begin
            for (r = 0; r < 8; r = r + 1begin
                amm[r][c] = 0;
                for (k = 0; k < 8; k = k + 1)
                    amm[r][c] = amm[r][c] + weight_data[r * 8 + k] * in_data[k * 8 + c];    // Weight x Input
            end
        end
        
        preload <= 1;                                   // Weight Preload 신호 High
        @ (negedge clk);                                // 첫 데이터를 출력하기 위해 대기
        for (c = 7; c >= 0; c = c - 1begin
            for (k = 0; k < 8; k = k + 1)
                weight[k] <= weight_data[k * 8 + c];    // 마지막 Column부터 Column 별로 Weight Data Load
            @ (negedge clk);                            // 한 Cycle마다 한 Column씩 처리
        end
        preload <= 0;                                   // Weight Preload 신호 Low
        
        mac <= 1;                                       // MAC 연산 수행 신호 High
        for (k = 0; k < 16; k = k + 1begin
            for (r = 0; r < 8; r = r + 1begin
                c = k - r;                              // 현재 사이클에서 r번째 행에 들어가야 할 열 계산
                if (c >= 0 && c < 8)
                    in[r] <= in_data[r * 8 + c];        // c가 0~7 사이일 때만 유효한 데이터 할당
                else
                    in[r] <= 0;                         // 아직 차례가 아니거나 끝난 경우 0
            end
            @ (negedge clk);                            // 대기
        end
        repeat(10) @ (posedge clk);                     // 파이프라인에 남은 마지막 데이터가 빠져나올 때까지 대기
        mac <= 0;                                       // MAC 연산 수행 신호 Low
        $finish;                                        // Simulation 종료
    end
endmodule
 
cs

· Verilog 방식으로는 Module 내부에서 2차원으로 받아도 상관없지만 2차원 Port로 입력 받기는 어려워서 System Verilog로 진행
· [7:0] in, weight [0:7] : 한 Cycle에 8개의 데이터만 받아서 데이터가 흐르도록 설계
· [17:0] out [0:7] : 8-bit Weight와 8-bit Input을 곱하고(16-bit), 8번 누적(3-bit)하므로 최소 19-bit
    → 실험 데이터 범위 내에서 18-bit로 최적화
· in_data[0:63], weight_data[0:63] : txt 파일의 Input Data와 Weight Data를 1차원 배열로 Load하여 저장하는 변수
· amm[0:7][0:7] : 실제 행렬 곱을 연산하여 설계한 HW로 연산한 결과와 비교

① Instance를 통해 pe_8x8.v Module과 연결
② Clock을 2ns 주기로 설정
③ rst = High, preload & mac = Low → 초기화 진행
④ txt 파일에서 Input Data와 Weight Data를 1차원 배열 형태로 Load
⑤ (Weight) x (Input)을 계산하여 설계한 Module의 연산 결과와 비교할 정답 행렬 생성
⑥ Weight Preload 신호를 켜고, 마지막 Column부터 Column 별로 Weight Data를 weight 변수에 Load
    → 한 Cycle마다 한 Column씩 처리되도록 대기
⑦ MAC 신호를 켜고, 데이터가 Skewing돼서 PE에 들어갈 수 있도록 k를 사용
    → 유효한 Column 범위가 아닌 경우(아직 차례가 아니거나, 입력이 끝난 경우) in 값으로 0을 할당
⑧ Pipeline에 남은 마지막 데이터가 Flush 되도록 Clock을 반복하며 대기
⑨ Simulation 종료

▣ pe_8x8.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
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
// System Verilog로 진행
`timescale 1ns / 1ps
module pe_8x8(
    input clk,                                                  // Clock 신호
    input rst,                                                  // Reset 신호
    input preload,                                              // Weight Preload 신호
    input mac,                                                  // MAC 연산 수행 신호
    input signed [7:0in[0:7],                                 // 8-bit Signed 값 8개
    input signed [7:0] weight[0:7],                             // 8-bit Signed 값 8개
    output reg signed [17:0] out[0:7]                           // 18-bit Signed 값 8개
);
    reg signed [7:0] pe_weight [0:7][0:7];                      // PE Weight Preload
    reg signed [7:0] in_reg [0:7][0:7];                         // 오른쪽으로 전달되는 In Data
    reg signed [17:0] psum_reg [0:7][0:7];                      // 아래로 전달되는 Partial Sum
 
    integer r, c;
 
    always @(posedge clk or posedge rst) begin
        if (rst) begin                                          // Reset 신호가 High인 경우
            for (r = 0; r < 8; r = r + 1) begin
                out[r] <= 0;                                    // out의 모든 값 초기화
                for (c = 0; c < 8; c = c + 1) begin
                    pe_weight[r][c] <= 0;                       // PE Weight 초기화
                    in_reg[r][c] <= 0;                          // In Register 초기화
                    psum_reg[r][c] <= 0;                        // Partial Sum Register 초기화
                end
            end
        end
        else begin
            if (preload) begin                                  // Preload 신호가 High인 경우
                // tb에서 Weight를 마지막 Column부터 Column 별로 데이터를 가져오므로 Transpose 됨
                for (c = 0; c < 8; c = c + 1) begin         
                    for (r = 7; r > 0; r = r - 1)
                        pe_weight[r][c] <= pe_weight[r-1][c];   // 위의 행의 데이터를 전달 받음
                    pe_weight[0][c] <= weight[c];               // 0번째 행은 새로운 Weight 할당
                end
            end
            
            else if (mac) begin                                 // MAC 신호가 High인 경우
                for (r = 0; r < 8; r = r + 1) begin
                    for (c = 0; c < 8; c = c + 1) begin
                        if (c == 0)                             // 0번째 Column인 경우
                            in_reg[r][c] <= in[r];              // 새로운 In Data 할당
                        else                                    // 0번째 Column이 아닌 경우
                            in_reg[r][c] <= in_reg[r][c-1];     // 왼쪽 In Data를 전달 받음
 
                        if (r == 0) begin                       // 0번째 Row인 경우
                            // 0번째 Column이면 새로운 In Data | 0번째 Column이 아니면 왼쪽 In Data와 곱함
                            psum_reg[r][c] <= pe_weight[r][c] * ((c == 0) ? in[r] : in_reg[r][c-1]);
                        end else begin                          // 0번째 Row가 아닌 경우
                            // 0번째 Column이면 새로운 In Data | 0번째 Column이 아니면 왼쪽 In Data와 곱해서 누적
                            psum_reg[r][c] <= psum_reg[r-1][c] + pe_weight[r][c] * ((c == 0) ? in[r] : in_reg[r][c-1]);
                        end
                    end
                end
            end
        end
    end
 
    always @ (*) begin
        for (c = 0; c < 8; c = c + 1) begin
            out[c] = psum_reg[7][c];                        // 마지막 행에서 계산된 Partial Sum은 해당 Column의 최종 결과
        end
    end
endmodule
cs

· Verilog 방식으로는 Module 내부에서 2차원으로 받아도 상관없지만 2차원 Port로 입력 받기는 어려워서 System Verilog로 진행
· in, weight [0:7] : 한 Cycle에 8개의 데이터만 받아서 데이터가 흐르도록 설계
· pe_weight : PE에 Weight를 Preload하는 변수
· in_reg : Input Data를 오른쪽으로 전달하는 변수
· psum_reg : Partial Sum을 아래로 전달하는 변수

> Reset 신호가 High인 경우
· out, pe_weight, in_reg, psum_reg를 모두 초기화

> Preload 신호가 High인 경우
· 0번째 Row가 아닌 경우 : 위 PE의 Weight를 전달받음 / 0번째 Row인 경우 : "weight"값을 할당 받음
    → "weight"는 tb_pe_8x8.v에서 마지막 Column부터 Column 별로 데이터를 Load
    → pe_weight의 0번째 행에 Column 별로 Load 한 데이터를 할당
    ∴ Weight Data는 Transpose되어 PE에 Load
· tb_pe_8x8.v에서 Clock 대기를 통해 한 Column씩 데이터를 할당 → 총 8 Cycle이 지난 후에 8x8 PE에 모든 Weight가 Preload 됨

> MAC 신호가 High인 경우
· 0번째 Column인 경우 : tb_pe_8x8.v에서 Skewing 되어 들어오는 Input Data를 할당
· 0번째 Column이 아닌 경우 : 왼쪽 PE의 Input을 전달받음
· 이후 Partial Sum 계산
    · 0번째 Row & 0번째 Column : 새로운 Input을 Weight와 곱셈
    · 0번째 Row & n번째 Column : 왼쪽 PE에서 전달받은 Input을 Weight와 곱셈
    · n번째 Row & 0번째 Column : 새로운 Input을 Weight와 곱한 후, 위에서 전달받은 이전의 Partial Sum 값에 누적
    · n번째 Row & n번째 Column : 왼쪽 PE에서 전달받은 Input을 Weight와 곱한 후, 위에서 전달받은 이전의 Partial Sum 값에 누적
→ 마지막 행에서 계산된 Partial Sum은 누적이 완료된 것이므로 out으로 출력

[Result]

· Weight Data가 마지막 Column부터 Column 별로 Data가 Load
· Input Data는 행 별로 Skewing 되어 들어가고 모든 값이 들어간 후에는 다시 0을 출력

· 설계한 HW로 계산한 결과와 SW로 계산한 결과가 동일 = 행렬 곱 연산이 제대로 진행
· 곱 연산이 Pipeline으로 잘 진행되며 Input에 대한 곱 연산이 완료된 후에는 0을 출력

[Reference]

· 8x8_PE_7 (Embedded System Lab : Chapter7) - William J. Song

댓글

이 블로그의 인기 게시물

[Mini-NPU RTL] NN Reference Model

[Mini-NPU RTL] TPU (Study Paper)