Main Content

이 페이지의 최신 내용은 아직 번역되지 않았습니다. 최신 내용은 영문으로 볼 수 있습니다.

GPU 성능 측정 및 개선하기

GPU 벤치마킹 시작하기

MATLAB®에서 다양한 벤치마크 테스트를 사용하여 GPU의 성능을 측정할 수 있습니다.

  • MATLAB Central File Exchange에 있는 gpuBench를 사용하여, 메모리를 많이 사용하는 작업과 계산량이 많은 작업을 포함한 다양한 테스트를 단정밀도와 배정밀도로 수행할 수 있습니다. 디스플레이 카드의 성능을 계산 카드와 비교합니다. 자세한 내용은 https://www.mathworks.com/matlabcentral/fileexchange/34080-gpubench 항목을 참조하십시오.

  • Measuring GPU Performance에서 paralleldemo_gpu_bench 스크립트를 사용하여 PCI 버스 속도, GPU 메모리 읽기/쓰기 및 배정밀도 행렬 계산의 피크 계산 성능에 대한 정보를 얻을 수 있습니다.

단정밀도 계산을 사용하여 성능 개선하기

배정밀도 대신 단정밀도로 계산을 수행하여 GPU 성능을 개선할 수 있습니다. 반면, CPU 계산에서는 배정밀도에서 단정밀도로 전환하면 이러한 성능 개선 효과를 얻을 수 없습니다. 그 이유는 대부분의 GPU 카드는 높은 단정밀도 성능이 필요한 그래픽 디스플레이용으로 설계되었기 때문입니다.

GPU에서 단정밀도 계산에 적합한 계산의 일반적인 예로는 이미지 처리 및 머신러닝이 있습니다. https://www.mathworks.com/content/dam/mathworks/tag-team/Objects/d/Deep_Learning_in_Cloud_Whitepaper.pdf를 참조하십시오. 그러나 선형 대수 문제와 같은 다른 유형의 계산에는 일반적으로 배정밀도 처리가 필요합니다.

단정밀도 계산은 배정밀도 계산에 비해 GPU 카드 및 총 코어 수에 따라 최대 50배까지 성능이 개선될 수 있습니다. 고급 계산 카드는 일반적으로 성능 개선 효과가 이보다 적습니다. gpuBench를 사용하여 특정 GPU의 성능 개선을 확인할 수 있습니다. https://www.mathworks.com/matlabcentral/fileexchange/34080-gpubench 항목을 참조하십시오.

NVIDIA® GPU 카드의 종합적인 성능 개요는 https://en.wikipedia.org/wiki/List_of_Nvidia_graphics_processing_units를 참조하십시오. 단정밀도와 배정밀도 간의 성능 개선도를 다음과 같이 계산할 수 있습니다.

  • 위의 위키 페이지에서 GPU를 찾습니다.

  • 표에 표시된 단정밀도 성능 값과 배정밀도 성능 값을 확인합니다. 배정밀도 GFLOPS 값이 없는 경우 배정밀도가 24‐32배 느린 것으로 가정합니다.

  • 표시된 단정밀도 GFLOPS 값을 배정밀도 GFLOPS 값으로 나눕니다.

참고

랩탑에 모바일 그래픽스 카드가 있는 경우 이 카드를 GPU 연산에 사용할 수 있습니다. 그러나 랩탑 GPU는 데스크탑 컴퓨터 장비보다 훨씬 덜 강력하므로 성능이 감소됩니다.

성능 향상을 위한 기본 워크플로

MATLAB에서 GPU 연산의 목적은 응용 프로그램의 속도를 향상시키는 것입니다. 여기에서는 GPU 하드웨어의 구성 및 코드의 모범 사례 등 GPU에서 더 나은 성능을 얻을 수 있는 기본 개념과 사례에 대해 설명합니다. 구현의 어려움과 성능 간의 상호 절충 관계에 대해 설명하고 gpuArray 함수, arrayfun, MEX 파일 또는 CUDA 커널을 선택할 때 사용할 수 있는 기준을 설명합니다. 마지막으로 GPU에서 성능을 정확하게 측정하는 방법을 설명합니다.

MATLAB 코드를 GPU에서 실행되도록 변환할 때는 이미 잘 수행되는 MATLAB 코드부터 시작하는 것이 가장 좋습니다. GPU와 CPU의 성능 특징은 다르지만 좋은 MATLAB 코드를 작성하기 위한 일반적인 지침은 GPU를 대상으로 좋은 MATLAB 코드를 작성하는 데에도 도움이 됩니다. 첫 번째 단계는 늘 그렇듯이 CPU 코드를 프로파일링하는 것입니다. CPU에서 대부분의 시간을 보내는 것으로 프로파일러가 표시한 코드 라인이 사용자가 GPU를 위한 코드를 작성할 때 집중해야 할 부분입니다.

gpuArray 데이터를 지원하는 MATLAB 내장 함수를 사용하여 코드 변환을 시작하는 것이 가장 쉬운 방법입니다. 이러한 함수는 gpuArray 입력값을 가져와 GPU에서 계산을 수행한 후 gpuArray 출력값을 반환합니다. gpuArray 데이터를 지원하는 MATLAB 함수 목록은 GPU에서 MATLAB 함수 실행하기에 나와 있습니다. 일반적으로 이러한 함수는 CPU에서 계산되는 표준 MATLAB 함수와 동일한 인수 및 데이터형을 지원합니다.

사용할 모든 함수가 GPU에서 지원되는 경우, GPU에서 코드를 실행하는 것은 입력 데이터를 GPU로 전송하기 위해 gpuArray를 호출하고 완료 시 GPU에서 출력 데이터를 가져오기 위해 gather를 호출하는 정도의 간단한 일이 될 수 있습니다. 많은 경우, 루프를 사용한 스칼라 연산을 MATLAB 행렬과 벡터 연산으로 대체하여 코드를 벡터화해야 할 수 있습니다. 벡터화는 CPU에서는 일반적으로 좋은 규범에 불과하지만 GPU에서는 높은 성능을 얻는 데 있어 극히 중요한 요소가 됩니다. 자세한 내용은 GPU 성능 향상을 위해 벡터화하기 항목을 참조하십시오.

성능 향상을 위한 고급 툴

입력값을 gpuArray로 변환하고 코드를 벡터화한 후에도 알고리즘에 내장 함수가 아니거나 응용 프로그램의 요구 조건을 충족하지 못할 정도로 느린 연산이 있을 수 있습니다. 이 경우 주로 세 가지 선택이 가능한데, arrayfun을 사용하여 응용 프로그램에서 요소별로 동작하는 부분을 미리 컴파일하거나 GPU 라이브러리 함수를 사용하거나 사용자 지정 CUDA 커널을 작성하는 것입니다.

순수하게 요소별로 동작하는 함수가 있는 경우 arrayfun을 사용하여 이 함수를 호출하면 성능을 개선할 수 있습니다. GPU에서 arrayfun 함수는 요소별 MATLAB 함수를 사용자 지정 CUDA 커널로 전환하므로 연산 수행에 대한 오버헤드가 줄어듭니다. 대개, 전체 응용 프로그램은 아니더라도 그 일부분에는 arrayfun을 사용할 수 있는 경우가 많습니다. 예제 Improve Performance of Element-wise MATLAB® Functions on the GPU using ARRAYFUN에서는 이 접근 방법의 기본 개념을 보여주고 예제 Using GPU ARRAYFUN for Monte-Carlo Simulations에서는 재무 응용 프로그램에 대한 시뮬레이션에서 어떻게 이를 수행할 수 있는지 보여줍니다.

MATLAB은 Parallel Computing Toolbox™, Image Processing Toolbox™, Signal Processing Toolbox™ 및 기타 제품에서 광범위한 GPU 지원 함수 라이브러리를 제공합니다. 그러나 MATLAB의 GPU 지원에서와 유사한 직접 내장 형태가 아닌, 추가적인 함수 라이브러리가 많습니다. 예로는 NPP(NVIDIA Performance Primitives) 라이브러리와 CURAND 라이브러리가 있으며 이러한 라이브러리는 MATLAB과 함께 제공하는 CUDA 툴킷에 포함되어 있습니다. 이러한 라이브러리 중 하나에 있는 함수를 호출해야 하는 경우 GPU MEX 인터페이스를 사용하여 호출할 수 있습니다. 이 인터페이스를 사용하면 MATLAB gpuArray에서 장치 데이터에 대한 포인터를 추출해 GPU 함수로 전달할 수 있습니다. 반환된 값을 gpuArray로 변환해 MATLAB에 반환할 수 있습니다. 자세한 내용은 CUDA 코드가 포함된 MEX 함수 실행하기 항목을 참조하십시오.

마지막으로, 필요한 연산을 위해 사용자 지정 CUDA 커널을 작성하는 방법이 있습니다. 이러한 커널은 CUDAKernel 객체를 사용하여 MATLAB에 직접 통합할 수 있습니다.

예제 Illustrating Three Approaches to GPU Computing: The Mandelbrot Set에서는 이 섹션에서 설명한 세 가지 접근 방법을 사용하여 간단한 계산을 구현하는 방법을 보여줍니다. 이 예제에서는 GPU에서 실행되도록 쉽게 변환되는 MATLAB 코드부터 제시하고, 요소별 연산을 위해 arrayfun을 사용하도록 코드를 재작성하고, 마지막으로 동일한 연산에 대해 사용자 지정 CUDA 커널을 통합하는 방법을 보여줍니다.

또는 MEX 파일의 일부로 CUDA 커널을 작성하고 MEX 파일 내에서 CUDA 런타임 API를 사용하여 이 커널을 호출할 수 있습니다. 이러한 방법 중 하나를 통해 공유 메모리 및 텍스처 메모리 등 MATLAB 코드에서는 직접 사용할 수 없는 하위 수준의 GPU 기능을 사용할 수 있습니다. 자세한 내용은 Accessing Advanced CUDA Features Using MEX 항목을 참조하십시오.

성능 향상을 위한 모범 사례

하드웨어 구성

일반적으로 GPU가 연산에만 사용될 때 최상의 성능을 얻을 수 있습니다. 문제가 차지하는 적절한 크기의 메모리 양과 시스템의 그래픽스 용도로의 지속적인 장치 사용 때문에 일반적으로 동일한 GPU 장치를 계산과 그래픽스 둘 모두의 용도로 사용하는 것은 실용적이지 않습니다. 가능하면 별도의 그래픽스용 장치를 사용하십시오. 계산용 장치 또는 그래픽스용 장치를 구성하는 자세한 내용은 운영 체제 및 드라이버 버전에 따라 다릅니다.

Windows® 시스템에서 GPU 장치는 WDDM(Windows Display Driver Model) 모드와 TCC(Tesla Compute Cluster) 모드, 둘 중 하나일 수 있습니다. 최상의 성능을 구현하려면 연산에 사용되는 모든 장치가 TCC 모드에 있어야 합니다. 자세한 내용은 NVIDIA 문서를 참조하십시오.

NVIDIA의 최고 성능 계산 장치인 Tesla 제품군은 GPU 메모리를 읽고 쓸 때 ECC(오류 수정 코드)를 지원합니다. ECC의 목적은 일반적으로 동적 메모리를 읽거나 쓸 때 가끔 발생하는 비트 오류를 수정하는 것입니다. 성능을 개선하는 한가지 기법은 ECC를 해제하여 사용 가능한 메모리 대역폭을 늘리는 것입니다. 이런 식으로 하드웨어를 구성할 수는 있지만 MathWorks에서는 이 방법을 권장하지 않습니다. 드러나지 않은 오류로 인한 잠재적인 정확도 손실이 성능상의 이점보다 더 해로울 수 있기 때문입니다.

MATLAB 코딩 사례

여기에서는 GPU에서 더 나은 성능을 얻을 수 있는 일반적인 기법을 설명합니다. 이 팁 중 몇 가지는 CPU를 위한 MATLAB 코드 작성 시에도 적용됩니다.

MATLAB 배열에서 데이터는 열 우선 순서로 저장됩니다. 따라서 배열의 첫 번째 차원이나 열 차원을 따라 연산을 수행하는 것이 득이 될 수 있습니다. 데이터의 한 차원이 다른 차원보다 훨씬 긴 경우 이 차원을 첫 번째 차원으로 하면 더 나은 성능을 얻을 수 있습니다. 마찬가지로, 특정 차원을 따라 빈번하게 연산을 수행하는 경우에는 일반적으로 그 차원을 첫 번째 차원으로 하는 것이 가장 좋습니다. 일부 경우에, 연속된 연산이 배열의 다른 차원을 대상으로 할 때는 이러한 연산 간에 배열을 전치하거나 치환하는 것이 유용할 수 있습니다.

많은 결과값을 병렬로 계산함으로써 GPU로 높은 성능을 얻을 수 있습니다. 따라서 행렬 연산 및 이보다 더 높은 차원의 배열 연산이 벡터 연산이나 스칼라 연산보다 일반적으로 성능이 더 좋습니다. 더 높은 차원의 연산을 활용하도록 루프를 재작성하면 더 나은 성능을 얻을 수 있습니다. MATLAB 행렬과 벡터 연산을 사용하도록 루프 기반의, 스칼라 위주의 코드를 수정하는 과정을 벡터화라고 합니다. 자세한 내용은 벡터화 사용 항목을 참조하십시오.

기본적으로 MATLAB에서 모든 연산은 배정밀도 부동소수점 산술로 수행됩니다. 그러나 대부분의 연산에서 정수형과 단정밀도 부동소수점 유형을 비롯한 다양한 데이터형을 지원합니다. 현재 GPU와 CPU는 일반적으로 단정밀도 연산을 수행할 때 처리량이 훨씬 많으며 단정밀도 부동소수점 데이터는 메모리를 덜 차지합니다. 응용 프로그램의 정확도 요구 사항에서 단정밀도 부동소수점 사용을 허용하는 경우 MATLAB 코드의 성능이 훨씬 향상될 수 있습니다.

GPU는 PCI 버스라고 하는 데이터 전송 메커니즘의 마지막에 위치합니다. 이 버스는 PC 호스트 메모리에서 다양한 확장 카드로 데이터를 전송하는 높은 대역폭의 효율적인 방식이지만 GPU 장치나 CPU의 전역 메모리에 대한 전체 대역폭보다는 여전히 훨씬 느립니다. 자세한 내용은 Measuring GPU Performance 예제를 참조하십시오. 또한 GPU 장치에서 MATLAB 호스트 메모리로 전송할 때 MATLAB은 다른 명령문을 실행하기 전에 장치에서 대기 중인 모든 연산이 완료되기를 기다려야 합니다. 이로 인해 응용 프로그램의 성능이 매우 저하될 수 있습니다. 일반적으로 MATLAB 작업 공간과 GPU 간에 데이터를 전송하는 횟수를 제한해야 합니다. 응용 프로그램을 시작할 때 GPU로 데이터를 한 번 전송할 수 있는 경우, GPU에서 수행할 수 있는 모든 계산을 수행한 다음 마지막에 결과를 다시 MATLAB으로 전송하는 것이 일반적으로 가장 좋은 성능을 발휘합니다. 마찬가지로, GPU에서 직접 배열을 만들 수 있는 경우 zeros와 같은 함수에 대해 'gpuArray' 또는 'like' 옵션을 사용합니다(예: 기존 gpuArray g에 대해 Z = zeros(___,'gpuArray') 또는 Z = zeros(N,'like',g) 사용).

GPU에 대한 성능 측정하기

GPU에 대한 성능을 측정하는 가장 좋은 방법은 gputimeit를 사용하는 것입니다. 이 함수는 입력 인수가 없는 함수 핸들을 입력값으로 사용하고 측정된 함수 실행 시간을 반환합니다. 더 정확한 결과를 얻기 위해 시간이 지정된 연산을 반복하고, 초기화 오버헤드를 방지하기 위해 측정 전에 함수를 실행하고, 시간 측정 함수의 오버헤드를 제거하는 등 벤치마킹에서 고려해야 할 사항들을 준수합니다. 또한 gputimeit는 최종 시간 측정 전에 모든 GPU 연산을 완료하도록 합니다.

예를 들어, 크기가 NxN인 확률 행렬 Alu 행렬 분해를 계산하는 데 걸리는 시간을 측정한다고 가정합니다. lu 행렬 분해를 수행하는 함수를 정의하고 gputimeit에 함수 핸들을 전달하여 이 작업을 수행할 수 있습니다.

A = rand(N,'gpuArray');
fh = @() lu(A);
gputimeit(fh,2); % 2nd arg indicates number of outputs

또한 tictoc을 사용하여 성능을 측정할 수도 있습니다. 그러나 GPU에서 정확히 시간을 측정하려면 toc을 호출하기 전에 연산이 완료될 때까지 기다려야 합니다. 이 작업을 수행하는 방법에는 두 가지가 있습니다. toc을 호출하기 전에 최종 GPU 출력 시점에서 gather를 호출할 수 있습니다. 이렇게 하면 걸린 시간을 측정하기 전에 모든 계산이 완료됩니다. 또는 gpuDevice 객체를 입력값으로 하는 wait 함수를 사용할 수 있습니다. 예를 들어, tic, tocwait을 사용하여 행렬 Alu 행렬 분해를 계산하는 데 걸리는 시간을 측정하려는 경우 다음과 같은 방법으로 수행할 수 있습니다.

gd = gpuDevice();
tic();
[l,u] = lu(A);
wait(gd);
tLU = toc();

또한 MATLAB 프로파일러를 사용하여 GPU 코드에서 계산 시간이 어떻게 분산되는지 표시할 수도 있습니다. 주의할 점은, 프로파일러는 시간을 측정하기 위해 각 코드 라인을 독립적으로 실행하기 때문에 평소의 연산 중에 발생할 수 있는 중첩(비동기) 실행은 프로파일러가 대응할 수 없다는 것입니다. 전체 알고리즘의 시간을 측정하려면 위에 설명된 대로 tictoc 또는 gputimeit를 사용해야 합니다. 또한 사용자가 정의한 MEX 함수를 비동기적으로 실행할 경우 프로파일은 올바른 결과를 산출하지 않을 수도 있습니다.

GPU 성능 향상을 위해 벡터화하기

이 예제에서는 CPU 대신 GPU에서 함수를 실행하고 계산을 벡터화하여 성능을 개선하는 방법을 보여줍니다.

행렬의 열에 대한 빠른 컨벌루션을 수행하는 함수를 가정하겠습니다. 신호 처리 응용 프로그램에서의 일반적 연산인 빠른 컨벌루션은 각 데이터 열을 시간 영역에서 주파수 영역으로 변환하고, 여기에 필터 벡터의 변환을 곱하고, 다시 시간 영역으로 변환한 후 결과를 출력 행렬에 저장합니다.

function y = fastConvolution(data,filter)
[m,n] = size(data);
% Zero-pad filter to the column length of data, and transform
filter_f = fft(filter,m);

% Create an array of zeros of the same size and class as data
y = zeros(m,n,'like',data);

% Transform each column of data
for ix = 1:n
    af = fft(data(:,ix));
    y(:,ix) = ifft(af .* filter_f);
end
end

이 함수를 CPU에서 특정 크기의 데이터에 대해 실행하고 MATLAB timeit 함수를 사용하여 실행 시간을 측정합니다. timeit 함수는 초기 시작 및 오버헤드와 같이 벤치마킹에서 일반적으로 고려해야 할 사항들을 준수합니다.

a = complex(randn(4096,100),randn(4096,100));  % Data input
b = randn(16,1);                               % Filter input
c = fastConvolution(a,b);                      % Calculate output
ctime = timeit(@()fastConvolution(a,b));       % Measure CPU time
disp(['Execution time on CPU = ',num2str(ctime)]);

표본으로 택한 컴퓨터에서 이 코드는 다음과 같은 출력값을 표시합니다.

Execution time on CPU = 0.019335

이제 GPU에서 이 함수를 실행해 보겠습니다. 입력 데이터를 일반 MATLAB 배열이 아닌 gpuArray로 변경하면 작업을 쉽게 수행할 수 있습니다. 함수 내에 출력값을 만들 때 사용되는 'like' 구문은 data가 gpuArray일 때 y가 gpuArray가 되도록 합니다.

ga = gpuArray(a);                              % Move array to GPU
gb = gpuArray(b);                              % Move filter to GPU
gc = fastConvolution(ga,gb);                   % Calculate on GPU
gtime = gputimeit(@()fastConvolution(ga,gb));  % Measure GPU time
gerr = max(max(abs(gather(gc)-c)));            % Calculate error
disp(['Execution time on GPU = ',num2str(gtime)]);
disp(['Maximum absolute error = ',num2str(gerr)]);

동일한 컴퓨터에서 이 코드는 다음과 같은 출력값을 표시합니다.

Execution time on CPU = 0.019335
Execution time on GPU = 0.027235
Maximum absolute error = 1.1374e-14

그러나 이 문제의 경우 GPU가 CPU보다 속도가 느립니다. 그 이유는 for 루프가 길이 4096의 개별 열에 대해 FFT, 곱셈 및 역 FFT 연산을 실행하기 때문입니다. 성능을 높이는 가장 좋은 방법은 단일 MATLAB 함수 호출이 더 많은 계산을 수행하도록 코드를 벡터화하는 것입니다. FFT 연산과 IFFT 연산은 벡터화하기 쉽습니다. fft(A)는 행렬 A의 각 열에 대한 FFT를 계산합니다. MATLAB 이진 스칼라 확장 함수 bsxfun을 사용하여 한 번에 행렬의 모든 열과 필터의 곱셈을 수행할 수 있습니다. 벡터화된 함수는 다음과 같습니다.

function y = fastConvolution_v2(data,filter)
m = size(data,1);
% Zero-pad filter to the length of data, and transform
filter_f = fft(filter,m);

% Transform each column of the input
af = fft(data);

% Multiply each column by filter and compute inverse transform
y = ifft(bsxfun(@times,af,filter_f));
end

벡터화된 함수를 사용하여 동일한 실험을 수행합니다.

a = complex(randn(4096,100),randn(4096,100));   % Data input
b = randn(16,1);                                % Filter input
c = fastConvolution_v2(a,b);                    % Calculate output
ctime = timeit(@()fastConvolution_v2(a,b));     % Measure CPU time
disp(['Execution time on CPU = ',num2str(ctime)]);

ga = gpuArray(a);                               % Move data to GPU
gb = gpuArray(b);                               % Move filter to GPU
gc = fastConvolution_v2(ga, gb);                % Calculate on GPU
gtime = gputimeit(@()fastConvolution_v2(ga,gb));% Measure GPU time
gerr = max(max(abs(gather(gc)-c)));             % Calculate error
disp(['Execution time on GPU = ',num2str(gtime)]);
disp(['Maximum absolute error = ',num2str(gerr)]);
Execution time on CPU = 0.010393
Execution time on GPU = 0.0020537
Maximum absolute error = 1.1374e-14

결론적으로 코드를 벡터화하면 CPU 버전과 GPU 버전 모두 더 빠르게 실행할 수 있습니다. 그러나 벡터화는 CPU 버전보다 GPU 버전에서 훨씬 효과가 큽니다. 향상된 CPU 버전은 원래보다 두 배 가까이 빠르고 향상된 GPU 버전은 원래보다 13배 더 빠릅니다. 원래 버전에서는 GPU 코드가 CPU보다 40% 느렸지만 수정된 버전에서는 약 5배 더 빠릅니다.

GPU 문제 해결하기

컴퓨터에 GPU가 하나만 있는 경우에는 그래픽스 카드도 디스플레이 카드처럼 동작합니다. 이 경우 OS(운영 체제)가 부과하는 제한 시간이 GPU에 적용될 수 있습니다. 다음을 통해 GPU에 대한 제한 시간을 확인할 수 있습니다.

gpuDevice
ans =
...
KernelExecutionTimeout: 1
KernelExecutionTimeout = 1이면 OS가 부과하는 제한 시간이 GPU에 적용되며, 따라서 OS가 갱신된 내용을 화면에 항상 표시할 수 있습니다. GPU 연산이 너무 오래 걸릴 경우에는 연산이 중단됩니다. 이 경우 GPU 연산을 무사히 재개하려면 MATLAB을 다시 시작해야 합니다.

참고 항목

관련 항목