We choose the Moon

MATLAB: aceleración de cálculo con GPU

Jorge García Tíscar| July 29, 2011

Un buen día, alguien pensó que igual que al principio eran los procesadores (las CPU) los que se ocupaban de renderizar los gráficos, podría ser una buena idea emplear las gráficas (las GPU) para realizar los cálculos del procesador. Inmediatamente, la gente de NVIDIA empezó a sufrir el conocido síndrome de dólares en los ojos: ($_$).

Como todo el mundo sabe, los gráficos de videojuegos hiperrealistas, la reproducción de vídeo en alta definición, el contenido 3D, etcétera, conllevan que las tarjetas gráficas sean una pieza de hardware impresionante, capaz de realizar cálculos muy complejos a muy alta velocidad.

Ahora hay no sólo gráficas, sino estaciones e incluso servidores y datacenters enteros dedicados a realizar cálculos con tarjetas gráficas, en los más diversos campos (fluidodinámica, imagen médica, finanzas, física), lo que se ha dado en llamar General Purpose GPU o GPGPU. Sin embargo para este tipo de usos se ofertan unas gráficas increíblemente caras: las NVIDIA Tesla y similares, absolutamente fuera (para variar) de un presupuesto modesto. Así que… ¿sirven las GPU normales para los cálculos cotidianos de unos simples estudiantes de ingeniería? Nos hemos propuesto averiguarlo!

Problema: sistema de n ecuaciones lineales

Para ello, vamos a investigar un problema de los más básicos: resolver un sistema de \(n\) ecuaciones lineales. Este tipo de problema es el más habitual en cálculos de ingeniería, como por ejemplo cálculo estructural mediante el método de los elementos finitos (MEF). En general, tendrá esta pinta:

%%% \left\{ \begin{array}{l} a_{11}x_1+a_{12}x_2+\cdots +a_{1n}x_n=b_1\\ a_{21}x_1+a_{22}x_2+\cdots +a_{2n}x_n=b_2\\ \cdots\\ a_{n1}x_1+a_{n2}x_2+\cdots +a_{nn}x_n=b_n \end{array} \right. %%%

Lo cual se puede expresar en forma matricial como \(A\cdot\vec x=\vec b\), donde:

%%% A=\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nn} \end{bmatrix}, \quad \vec{x} = \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{bmatrix}, \quad \vec{b} = \begin{bmatrix} b_1 \\ b_2 \\ \vdots \\ b_n \end{bmatrix} %%%

Así que la solución sería de la forma \(\vec x=A^{-1}\vec b\). Implementaremos esta solución en Matlab (oh, sorpresa! seguro que nadie se lo esperaba!) y veremos cuál es la manera más rápida de calcularla, para distintos valores de \(n\).

Matlab: tiempo CPU vs. tiempo GPU

Nuestro objetivo será comparar el tiempo de cálculo resultante de usar la CPU normalmente con el tiempo requerido empleando cálculo GPU, para un rango de tamaños del problema (valores de \(n\)). Para ello empezamos definiendo dicho rango e inicializando las variables:

1
2
3
4
5
6
7
% orden máximo del sistema y paso
% del rango de valores
nmax = 5000;
paso = 50;
% inicializar variables
t0 = zeros(round(nmax/paso),1);
t1 = zeros(round(nmax/paso),1);

A continuación calculamos el tiempo que se tarda en resolver el sistema normalmente (línea resaltada) mediante el comando \; o función mldivide() de Matlab, que en función de la matriz \(A\) realiza una descomposición LU, QR o, en caso de sistemas sin solución exacta, una aproximación por mínimos cuadrados. Los valores del sistema se generan aleatoriamente. Los tiempos se guardan en el vector \(t_0\):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
% cálculo de tiempos CPU
% sirve de base para comparar
i = 1;
for n = 1:paso:nmax
    % creamos la matriz de coeficientes
    % y el vector de términos independientes
    b = rand(n,1);
    A = rand(n,n);
    % medimos el tiempo de resolución
    crono = tic;
    x = A\b;
    t0(i) = toc(crono);
    % imprimimos el n actual
    clc;fprintf('CPU: %d\n',n)
    i = i+1;
end

Una vez establecida la base de comparación, probaremos con la GPU. Para ello, al generar la matriz \(A\) y el vector \(\vec b\), debemos pasarlos a la GPU mediante el método gpuArray(), con lo que la operación \ la realizará la GPU. Una lista de las funciones soportadas se puede consultar en la documentación de la Parallel Toolbox.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
% cálculo de tiempos CPU
i = 1;
for n = 1:paso:nmax
    % creamos la matriz de coeficientes
    % y el vector de términos independientes
    % como matrices en la GPU
    b = gpuArray(rand(n,1));
    A = gpuArray(rand(n,n));
    %medimos el tiempo de ejecución
    crono = tic;
    x = A\b;
    t1(i) = toc(crono);
    % imprimimos el n actual
    clc;fprintf('GPU: %d\n',n)
    i = i+1;
end

Por último, ya podemos realizar el postproceso: salvar los datos que nuestro buen tiempo nos ha costado generar, preparar una figura con los resultados, y guardarla en el formato que deseemos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% salvamos los datos medidos
save('datos_n5000','t0','t1',)

% preparamos los datos a graficar
p = 1:paso:nmax;
comp1 = (t1-t0)./t0*100;

% dibujamos el gráfico
f = figure();

plot(p,comp2,p,0)
set(gca,'YLim',[-1000 1000])
set(gca,'YTick',-1000:500:1000)

xlabel('Orden n del sistema')
ylabel('tiempo GPU vs. tiempo CPU')
grid on

% guardamos el gráfico
savefigure('figura','s',[4.5 3],'po','-dpdf')

Resultados de la prueba

Una vez establecido el objetivo y el método a emplear, llega el momento de aportar datos concretos. El ordenador en el que se ha realizado la prueba tiene las siguientes características:

Utilizando el código anterior para tamaños del problema desde \(n=1\) a \(n=5000\) (es decir, desde resolver una ecuación de una incógnita a resolver 5000 ecuaciones con 5000 incógnitas) el resultado ha sido el siguiente:

Podemos observar que para tamaños pequeños del problema la GPU es terriblemente ineficiente, es como intentar matar moscas a cañonazos. Sin embargo, parece que para tamaños mayores de \(n=1000\) mejora, pasando a un cierto valor asintótico de mejora. Ajustando la escala:

Observamos que efectivamente, para tamaños \(n>1000\), la GPU reduce el tiempo de cálculo entre un 20% y un 40%. Teniendo en cuenta que esta mejora se ha logrado empleando una gráfica de gama media (alrededor de 100€) y simplemente pasando las variables a calcular a la GPU con un simple y único comando, parece evidente que hasta para aplicaciones simples, las GPGPU tienen mucho que decir.

avatar Thanks for reading! To share this post, use this permalink

Comments