GEMM이 빨라진 뒤에도 남는 모델 실행 경로
행렬 곱이 네 배 빨라졌는데 모델 전체의 시간은 절반 정도만 줄었다. accelerator가 기대한 일을 안 한 것일까. 모델의 비행렬 연산 경로를 따라가 보면, 행렬 결과를 다음 연산이 쓸 수 있게 바꾸는 작업이 남아 있다. activation과 reduction, 형식 변환과 이동은 MAC 처리량이 높아져도 자기 경로에서 시간을 쓴다.
graph의 다음 화살표도 실행해야 한다
한 layer가 행렬 곱 결과에 bias와 ReLU를 적용하고 다음 layer에 넘긴다고 하자. ReLU는 원소마다 max(x,0)을 만들므로 각 결과 원소가 준비될 때 적용할 수 있다. 이를 행렬 결과를 읽는 경로에 붙이면 별도 buffer로 중간 tensor를 쓰고 다시 읽는 일을 줄일 여지가 있다.
Gemmini 원문은 accumulator에서 작은 입력 타입으로 값을 읽을 때 scaler와 activation 회로를 적용하고, max-pooling은 store 경로에서 처리한다고 설명한다. 이런 주변 회로도 실제 데이터 경로의 일부다. 다만 기능이 optional이거나 생성한 구성에 달린 경우, 어떤 이름의 연산이 존재한다는 사실만으로 원하는 모든 속성·정밀도의 operator를 실행한다고 판단하지 않는다. Gemmini, Scratchpad and Accumulator 및 Decoupled Access/Execute
처리할 수 없는 연산을 host CPU에 넘긴다면 결과를 CPU가 볼 수 있는 곳으로 옮기고, 계산을 마친 뒤 다음 가속기 작업이 볼 수 있게 해야 한다. 연산 하나의 산술 시간 외에 실행 경계를 건너는 시간이 붙는다. tensor 크기와 layout, coherence 및 동기화 경로도 그 비용을 바꾼다.
네 배 빨라진 부분을 전체 시간에 넣어 본다
겹침이 없는 가상의 요청 하나에서 행렬 계산이 8ms, 비행렬 계산이 2ms, 두 경로 사이의 이동·동기화가 1ms라고 정하자. 총 11ms다. 행렬 계산만 네 배 빨라지면 2+2+1=5ms가 되고 전체 speedup은 11/5=2.2배다.
행렬 계산 시간을 0으로 만드는 가상 극한에서도 남은 3ms 때문에 speedup은 11/3, 약 3.67배를 넘지 않는다. 이 숫자는 실제 모델 측정값이 아니라 직렬로 더한 시간 예다. 데이터 이동이 계산과 겹치거나 공유 bandwidth 때문에 서로 방해한다면, 전체 시간을 이렇게 세 항의 단순 합으로 표현할 수 있는지부터 확인한다.
비행렬 계산의 2ms가 무엇인지도 중요하다. 행렬 결과에 가벼운 원소별 연산을 하는 경우와, 큰 범위의 통계를 구한 뒤 모든 원소를 다시 바꾸는 경우는 같은 주변 회로로 처리하기 어렵다. 이름만 보면 둘 다 짧은 후처리처럼 보여도 의존 관계가 다르다.
현재 입력의 통계가 필요한 연산은 뒤쪽 값을 기다린다
입력 네 값 [2,4,6,8]에 대해 현재 입력의 평균과 분산으로 정규화한다고 하자. 평균은 5, population variance는 ((−3)²+(−1)²+1²+3²)/4=5다. affine 후처리 없이 output을 (x−5)/sqrt(5+ε)로 만들면 첫 값 2의 변환에도 뒤의 4,6,8이 필요하다.
첫 원소가 나오자마자 정규화한 최종값을 확정하는 것은 이런 의존 관계와 맞지 않는다. 통계를 구하는 동안 값을 보관하거나 다시 읽는 경로가 필요하다. 통계 자체는 streaming 방식으로 구할 수 있어도, 필요한 통계가 준비되기 전의 정규화 output을 자동으로 확정하지는 못한다.
반면 inference에서 미리 정해진 통계를 사용하는 정규화는 현재 입력 네 값의 통계를 구하는 예와 다르다. 고정된 affine 변환으로 정리할 수 있는 경우가 있다. 같은 normalization이라는 이름 아래에서 어느 축의 어떤 통계를 사용하는지 읽어야 구현 경로를 선택할 수 있다.
fusion은 중간 tensor와 의존성을 함께 바꾼다
TVM의 원 논문은 operator fusion을 중간 결과를 별도 memory에 저장하지 않고 여러 operator를 한 kernel로 묶는 방식으로 설명한다. 원소별 map과 reduction, convolution 출력에 붙는 원소별 연산을 구분하고 결합 규칙을 둔다. 같은 graph의 화살표라도 어디까지 결합하는지에 따라 실현 가능한 재사용이 달라진다. TVM 논문, 3–4쪽 Operator Fusion
fusion이 통계의 데이터 의존성을 없애는 것은 아니다. 한 kernel 안에서도 값 보관이나 여러 단계가 필요할 수 있다. 모든 중간값을 register에 넣으려다 resource 사용이 늘어나거나, 기존 accelerator가 해당 결합을 지원하지 않으면 예상과 다른 결과가 나온다. fuse했다는 표시와 실제 감소한 memory 접근을 따로 확인한다.
비행렬 연산의 전용 unit을 더하는 선택도 실행 빈도와 공유 자원을 고려한다. 자주 쓰는 원소별 변환의 경로를 붙이는 것과 드문 복잡 연산을 모두 hardware로 옮기는 것은 다른 비용 문제다. host fallback이 남는 graph에서는 두 경로를 왕복하는 횟수를 줄이는 배치가 unit 하나의 속도보다 유리할 수도 있다.
모델 성능을 읽을 때 행렬 구간의 peak와 전체 graph의 완료 시간을 함께 기록한다. 각 비행렬 operator가 어디서 실행되고, 입력이 언제 준비되며, 다음 consumer가 어떤 형태로 읽는지를 연결한다. 이 경로를 보아야 빠른 GEMM이 실제 요청의 완료 시간을 얼마나 줄였는지 설명할 수 있다.