Translate

2021년 9월 3일 금요일

2장. 실습 3: HLS 설계 최적화(Design Optimization)

2장. 실습 3: HLS 설계 최적화(Design Optimization)

HLS로 합성된 설계물의 성능을 높여본다. C 설계 소스에 #define 매크로를 이용해 합성 지시자(directives)를 넣거나 Tcl 스크립트로 작성할 수 있다. HLS 에서 C 에서 RTL로 변환시 합성 지시자로 크게 두가지를 고려해 볼 수 있다.

  • 인터페이스(interface): 외부 모듈과 데이터를 주고 받기 위한 절차
  • 처리량(throughput): 입력이 주어지고 출력을 얻기까지 소요되는 클럭 수

1. 인터페이스

인터페이스는 FSM(Finite State Machine)으로 구현되어 설계의 일부로 포함된다. C 의 함수에 명시 되지 않은 인터페이스를 적용될 하드웨어마다 RTL/HDL 로 따로 준비해 두고 관리하기 어렵다. 따라서 인터페이스(INTERFACE)를 지정만 하면 자동생성 해주는 HLS의 인터페이스 합성은 매우 유용한 도구다.

C 소스 파일을 열면 특성 창에 함수들의 목록과 인수들을 나열해 준다. 값으로 전달되는(call-by-value) 인수는 RTL의 입력 포트(input ports)로, 주소가 전달 되는(call-by-address) 포인터 인수는 출력 포트(output ports)가 된다. 각 포트에서 사용할 인터페이스를 선택하면 합성기는 그에 맞는 제어 신호들과 절차를 FSM으로 자동생성한다. 인터페이스가 변경 되면 그에 맞는 HDL 테스트벤치도 재생성 되는데 이는 테스트 벤치의 자동화의 큰 장점이다. (아쉽게도 자동 생성된 인터페이스 테스트 벤치는 그리 신통치 않음!)

HLS 가 제공하는 인터페이스 옵션에는 일방형(Block I/O), 준비-확인형(RDY-ACK), 마스터- 슬레이브(Master-Slave)형을 비롯해 표준화된 온-칩 메모리의 접근이나 시스템 버스 AXI 를 선택할 수 있다.

설계 모듈이 외부와 데이터를 주고 받기 위한 방법으로 C 의 경우 파라메터 전달 그리고 리턴 값을 받는 함수 호출 규정이 전부다. RTL의 경우 핸드쉐이크, 시스템 버스, 마스터-슬레이브 라는 다양한 방법을 취할 수 있으며 그에 맞는 절차와 제어 신호를 동반한다. 이 절차를 프로토콜(protocol)이라 한다. 선택한 인터페이스의 절차가 다르므로 소요되는 클럭의 수도 다를 수 있다.

1-1. ap_vld 방식 인터페이스

특별히 인터페이스를 지정하지 않은 경우의 프로토콜은 가장 단순한 방식을 취한다. 대기신호(idle)를 내면 외부에서 유효 입력값과 함께 시작 신호(start)를 준다. DUT는 시작 신호를 받아 내부 처리를 개시한다. 외부에 입력을 받았다는 확인을 해주지 않으므로 start와 입력 x 를 전송 종료시 까지 유지하고 있어야 한다.

DUT가 처리를 마치면 유효 출력을 알리고(ready) 핸드 쉐이크 절차를 끝낸다(done). 외부에서 유효 출력을 받고자 대기 중 인지 확인하지 않고 일방적으로 종료한다. 설계 모듈 DUT가 전송의 개시와 종료에 대한 일방적 권한을 가진다.

1-2. ap_ack 방식 인터페이스

핸드쉐이크에 준비와 확인절차를 거치는 양방향 절차를 사용하는 편이 안전하고 다중처리에 용이하다. 상대로부터 유효한 데이터를 받고 이를 확인(acknowledge)해주면 인터벌 기간동안 상대가 다른 일을 할 수 있게 해줄 수 있다. DUT는 인터벌을 마치고 상대로부터 받을 준비가 되었음을 알려올 때를 기다려 유효한 출력을 내보낸다. 외부 모들은 DUT 가 몇 클럭만에 처리를 마치는지 모르므로 받을 준비가 되었음을 알려 주어야 한다.

인터페이스 절차를 수행하기 위해 유한 상태 머신(FSM, Finite-State Machine)을 구성하는데 대기와 학인(Rdy-Ack)을 위해 한개 이상의 상태를 차지해야 하므로 핸드쉐이크에 여분의 클럭을 필요하다. 온-칩 버스에 붙일 모듈이라면 더욱 복잡한 절차가 요구된다.

2. 처리량(throughput) 최적화

앞서 언급 되었던 인터벌 클럭수의 최소화다. 

C 언어의 변수(variables)와 연산자(arithmetic-logical operators) 그리고 조건문(if-condition)을 HDL로 변환하기는 어려울 것이 없다. 두 언어 모두 의미가 동일하기 때문이다. 다만 변수(variable)의 경우 연결선(wire) 또는 저장소(flip-flop)가 될 수 있다. 코딩 스타일에 의한 것일 수 있고 최적화의 결과로 나뉠 수도 있다. 하드웨어의 구조가 극적으로 바뀌는 경우는 반복문(loop)이다.

신호처리(DSP), 인공지능(AI) 등의 고급 알고리즘은 행렬계산에 전적으로 의존한다. 행렬 계산은 컴퓨터 언어의 반복문(for-loop)으로 구현된다. 수많은 반복문을 얼마나 빠르게 수행할 것인가가 오늘날 컴퓨팅 성능의 최대 관건이 되었다. C 언어로 기술된 알고리즘을 전용 RTL 하드웨어로 구현해 빠르게 수행하려면 결국 클럭 수를 최소화 하는 것이다.

2-1. 말려있는 반복문(Rolled Loop)

HLS는 C 설계에서 계산이 집중되는 반복문을 찾아내서 병렬성을 검출하고 이를 최적화 한다. 반복문에 최적화 지시를 하지 않고 합성 해보자.

다음은 성능 보고서의 일부다. 인터벌이 무려 27이다. 하드웨어 사용량은 의외로 작다. 특히 C 의 for 반복문 내에 32비트 레지스터가 10개나 필요한데 쉬프트 레지스터를 플립플롭으로 구현 했다면 320개의 플립플롭이 필요하다. 그런데 전체 플립플롭의 수가 476개로 나온 것은 너무나 적은 수다.

합성 보고서의 저장소 항목을 보면 shift_reg가 ram_1p 로 구현 된 것으로 보아 쉬프트 레지스터는 플립플롭을 사용하지 않았다.

또한 곱셈(mult) 연산이 반복문 내에 포함 되었지만 DSP 자원의 사용량도 매우 적다. 연산자 항목을 보니 1개의 곱셈기와 누산기가 사용됐을 뿐이다.


위의 내용으로 볼때 for 반복문은 말린채(rolled) 동작하고 내부 구조는 다음과 같을 것으로 추정된다.

2-2. 반복 풀기(UNROLL Loop)

HLS 합성기에게 말려있던 for 반복문을 풀도록(UNROLL) 지시해보자.

합성 보고서를 보니 레이턴시와 인터벌이 절반 이하로 줄었으나 하드웨어의 사용량이 급격히 증가했다.

하드웨어 자원 사용내역을 보니 11개의 곱셈기(mul)와 8개의 가산기(add) 그리고 1개의 누산기(acc) 가 사용 되었다.

외부의 c[N]이 두개의 메모리 인터페이스를 갖게 된 점이 눈길을 끈다. 왜 그랬을까?

엄청나게 늘어난 FF과 DSP 블럭 그리고 가산기 들을 고려해 보면 for 반복문을 풀면서 병렬처리 구조의 하드웨어를 구성한 것으로 보인다. 합성된 RTL/HDL을 읽어보고 문석할 수도 있겠으나 정신 건강상 해롭다. 위의 하드웨어 사용량과 인터벌을 참고하여 데이터 페스를 추정해 보면 다음과 같을 것이다.

동일한 두개의 메모리 인터페이스를 가지게 된 연유를 대략 짐작할 수 있다. 역시 외부의 c[N]이 병렬처리의 병목(bottle-neck)의 원인으로 보인다.

2-3. 배열 분리하기(ARRAY_PARTITION)

대규모 저장소를 확보할 수 있으나 주소-읽기/쓰기-데이터의 순차적인 절차를 준수해야 하는 메모리는 병렬 처리에 매우 불리하다. 메모리 인터페이스를 해결하려면 c[N]인 배열을 메모리가 아닌 개별 입력으로 분리해 내도록(ARRAY_PARTITION) 합성기에게 지시해보자.

합성 보고서에 따르면 하드웨어 사용량은 비슷하나 인터벌이 절반으로 줄었다.

메모리 인터페이스가 사라졌다. 하지만 입력 포트의 비트수가 엄청나게 증가했다.

상수 계수들이 일거에 준비 되었으므로 이제 열한번의 곱셈이 동시에 이뤄질 수 있게된 하드웨어를 가지게 되었다. 병렬 가산기를 몇단 더 추가하여 누산(accumulation)을 수행한다.

외부에서 입력되는 11개의 32비트 상수 때문에 입력 포트 와이어가 엄청나게 늘었다. ASIC 이라면 금속 층에 배선하므로 이정도 와이어 증가는 참을만 하다. 하지만 배선 용량을 고정되어 있는 FPGA의 경우 심각히 고려해야 한다. 배선용량이 모자라면 로직 블럭(LUT)을 통해 배선을 할 것이고 이는 최대지연 경로(max delay-path)를 늘리게 되어 동작속도를 심각하게 낮춘다.

2-4. 상수 곱셈

위의 fir()은 두 종류의 입력 포트를 가진다. 포트 c[]가 상수라는 점에 주목하자. fir(x, c[], *y) 로 주어진 경우 HLS 합성기는 c[]가 x와 마찬가지로 변수로 인식한다. 따라서 곱셈기는 변수와 변수의 곱셈기가 된다. 만일 c[]를 합성기에게 상수임을 노출 시키면 강력한 최적화를 수행한다. 상수의 유효 비트(dynamic range)에 따라 작은 크기의 곱셈기를 사용하거나 고정된 상수라면 단지 비트맵 변경을 통해 실제 곱셈기 없이 연산을 수행 할 수도 있다.

C의 함수 fir()에 적용될 필터 계수는 모두 상수배열 이었다. 특히 상수 중에 0이 포함되었을 뿐만 아니라 좌우 대칭이라는 점이 눈에 띈다. 0을 곱하는 곱셈기는 필요 없다. 따라서 두개의 거대한 곱셈기를 제거 할 수 있다.

상주중 가장 큰 수는 c[5]=63이다. 따라서 c 의 변화 범위(dynamic range)는 63이 2진수 (111111)이므로 7비트 이나, c[4]=c[6]=56 보다 7 차이가 난다. 7을 왼쪽으로 3비트 옮기면 56이다. 이런 단순한 대수(arithmetic)를 적용해 계수의 종류를 줄여 나가면 곱셈을 아예 제거 할 수도 있을 것이다.

계수의 종류를 줄여 나가다 보면 곱셈을 최소화 한 덧셈과 와이어 쉬프트로 계산을 끝낼 수도 있다. 결국 논리식 간략화의 문제로 귀결될 것이다. 상수 배열 c[]을 HLS에 노출 시키기 위해 fir() 설계를 변경했다.

UNROLL과 ARRAY_PARTITION 조건을 주고 HLS 합성 해보자.

지연은 목표치 내로 들어 왔으며 하드웨어 사용량도 매우 적어졌고 인터벌은 5다. 상수 계수 c[]가 노출되지 않았을 때와 비교해보면 매우 효과적인 최적화를 수행한 것을 알 수 있다.

여전히 1개의 거대한 곱셈기가 남았다. 이 곱셈기의 용도는 뭘까 궁금하다. 그 이유를 알기 위해 툴이 생성한 HDL 코드를 읽어볼 생각은 말자. 어디까지나 툴이 읽을 것을 전제로 생성되었다.

HDL 시뮬레이션으로 곱셈기의 입출력을 추적해 보니 시뮬레이션을 종료할 때까지 한 입력이 23으로 고정된 것으로 보아 최적화에 소수(prime number)를 간략화 하지 못한 것으로 보인다. 제아무리 소수라지만 곱셈기를 제거할 수도 있었을 것이다. 하지만 23을 제거하기 위해 여러단의 덧셈기로 바꾸기엔 유효 비트들이 많아서 인터벌이 길어질 수 있으므로 최적화의 타협 끝에 결정된 것으로 보인다.

만일 계수 c[5]를 24로 바꿨더라면 어떤 결과를 냈을까?

하드웨어에서 곱셈기는 사라졌고 인터벌도 줄었다.

3. 결론

HLS 툴의 합성기는 매우 훌륭했다. 최고의 효과를 얻으려면 C 알고리즘 작성시 구조와 최적화를 고려하자.

[소스파일 다운로드]

---------------------------------------------------------------------------------

고위 합성 튜토리얼(High-Level Synthesis Tutorial)
[목차][이전][다음]


댓글 없음:

댓글 쓰기