연구/논문 리뷰

[Arxiv] cuDNN: Efficient Primitives for Deep Learning

xeskin 2020. 6. 10. 14:30
반응형

Introduction

cuDNN은 엔비디아에서 제공하는 DNN primitives를 모아놓은 라이브러리다. 본문에서는 CNN이 기존 선형대수 라이브러리와는 달리 dense kernel을 이용해 계산하는 점을 지적하며, 이를 위해 만든 최적화된 primitives를 어떻게 만들었는지 소개한다.

 

Spatial Convolutions

CNN에서 가장 중요한 연산은 convolution이다. 이때 필요한 파라미터는 다음과 같다.

Convolution parameters

Convolution에는 input data와 convolutional filter 두 개의 input이 필요하다. input data는 미니 배치 내의 이미지 개수(N), 인풋 채널 개수(C), 이미지의 가로, 세로 (W,H)로 결정된다. convolutional filter는 아웃풋 채널 개수(K), 인풋 채널 개수(C), 필터의 가로, 세로 (S, R)로 결정된다. 그러면 아웃풋은 four-dimensional tensor가 되는데, CNN에서 어떻게 feature map size를 결정 했는 지 생각해보면 될 것이다. 구체적이 계산은 다음과 같다.

Height of output feature map

위의 식에서 u를 horizontal stride에 해당하는 v로 바꾸고, pad_h를 width of zero-padding에 해당하는 pad_w로 바꿔주면 output feature map의 width 또한 계산할 수 있다. convolution을 계산하려면 filter내의 weight이 input data의 위치에 정확하게 접근해야 하는데, 이를 맞춰주기 위해 accessing function을 정의했다.

Accesing function for convolution

이제 forward convolution을 계산할 수 있는데 이는 다음과 같다.

Forward convolution

Convolution을 계산하는 것은 4개의 independent loop와 3개의 accumulation loop가 있는 7-way nested loop인 것을 알 수 있다. O에 있는 n, k, p, q에 의해 인풋 데이터와 output form이 결정되면 채널 내에서 필터가 빙빙 돌아가며 계산하는 걸로 생각할 수 있다. (n, k, p, q는 각각 image, output feature map, height/width of output feature map을 의미한다.) 이제 이 연산을 어떻게 최적화할 것이냐가 핵심이 된다. 본문에서는 세 가지 접근법을 소개한다.

 

Approach

Author's approach

저자들은 image data와 filter data를 복사한 뒤에 concat하여 lower matrix를 만들어 elment-wise multiplication이 아닌 matrix multiplication 문제로 바꿨다고 한다. 여기서 최초로 제안된 접근법은 아니고 여기서 제안됐던 거라고 한다.

Convolution lowering

Matrix multiplication으로 바꾸면 이는 최적화할 수 잇는 방법들이 많아 효율적으로 계산할 수 있다. 그리고 matrix multiplication은 전송된 데이터 당 부동 소수점 연산 비율이 높기 때문에 연산이 빠른데, matrix가 커질수록 이 비율이 높아져 효율적으로 계산할 수 있지만 반대로 matrix size가 작으면 비율이 낮아져 효율성이 떨어지게 된다. 그럼 여기서 D_m과 F_m이 어떻게 sizing되냐가 중요할 것이다. 그런데 이건 파라미터 자체에 의존하기보다는 convolution을 하기위한 parameter의 product(CxRxS)에 의존하는 성질을 갖고 있다. (C: # of input feature map, R/S: Height/Width of kernel)

 

그러니까 product가 충분히 크면 개별적인 파라미터가 작더라도 성능 자체는 일관적이다는 것이다. 예를 들어 C가 작더라도 R, S가 크거나 R,S가 작더라도 C가 충분히 크면 product는 consistent할 것이다. 실제로 product는 모든 레이어 대해서 크기 때문에 성능은 일관적으로 잘 나온다고 한다.

 

하지만 단점도 있다. 우선 input data에 해당하는 D_m을 만들려면 input data를 최대 RS번 복사를 해야되는데 이는 메모리에 과부하를 줄 수 있다. 이를 해결하기 위해서 구현할 때 미니배치 내에 각 요소에 대해 matrix multiplication을 반복적으로 호출해서 D_m을 piece-by-piece로, 즉 하나씩 만든다고 한다. 하지만 이도 문제점이 있을 수 있는 것이 GPU를 fully utilize하기에 mat.mul이 작을 수 있고 input data를 읽는 동안에도 D_m을 생성하기 위해 읽고 써야하기 때문에 convolution computation intensity가 direct convolution보다 낮을 수도 있고 많은 memory traffic을 발생시킬 수 있다. 다 복사해야 되니까!

 

FFT (Fast Fourier Transform)

FFT는 convolution의 compexity를 많이 줄여줄 수 있지만 몇 가지 문제점이 있다고 한다. 첫 번째로 FFT는 filter를 input data와 같은 크기로 padding해야되서 일시적으로 많은 메모리가 필요하다. 즉, filter size가 input size보다 많이 작을 때 비효율이 크다. 몇몇 경량화 모델에 대해 1x1 convolution이 사용되는 것을 생각해보면 비효율이 큰 경우가 종종 있을 수 있다. 두 번째로 FFT는 striding size가 1보다 큰 경우에 효율적이지 못하다고 한다. Striding은 sparse output을 내놓는데 이에 대해 FFT를 적용하는 것은 non-trivial task라고 한다. 그리고 부가적인 샘플링이 필요해서 오히려 dense FFT보다 느릴 수 있다고 한다. 그래서 라이브러리에 추가하지는 않았다고...

 

Direct Convolution

다른 기법을 사용하지 않고 바로 convolution 계산하는 것도 효율적이지만 비효율적인 케이스가 많다고 한다. 예를 들어, cuda-convnet2의 경우에 배치 사이즈가 크면 좋은 성능을 내는데 64 이하면 성능을 못낸다. 모든 경우를 고려해 최적화해주기는 어렵지만 몇몇 케이스에 대해서 효율적인 경우가 있기 때문에 라이브러리에 넣어뒀다고 한다.

 

Implementation

엔비디아에서 제공하는 라이브러리의 matrix multiplication은 세 가지 스텝을 밟는다.

 

1. Input data A, B의 고정된 사이즈의 submatrix를 on-chip memory에 연속적으로 읽어온다.
2. submatrix를 연산하여 output matrix C의 submatrix를 내놓는다.
3. On-chip caches나 off-chip memory에서 submatrix를 읽어오는 동안 submatrix를 계산한다.

 

이 방법은 GPU가 연산하는 동안 데이터를 읽어오기 때문에 latency를 숨길 수 있는 장점이 있다. 그러면 이제 input data가 있으면 input data matrix D_m을 어디서 생성할 거냐가 문제가 된다.

 

Experiment

배치사이즈가 충분히 큰 경우에는 cuda-convnet2이 나은 성능을 보였지만 그 이하에서는 cuDNN이 좋은 성능을 보였다. 실제로 돌릴 때는 배치 사이즈가 얼마인지 체크하고 cuDNN 돌리던가 cuda-convnet2 돌리던 지 할 것 같다.

반응형