CUDA es bueno para todos, siempre que tenga a mano una tarjeta de video de Nvidia. Pero, ¿qué hacer cuando no hay una tarjeta gráfica Nvidia en su computadora portátil favorita? ¿O necesita realizar el desarrollo en una máquina virtual?
Intentaré considerar en este artículo una solución como el marco rCUDA (Remote CUDA), que ayudará cuando hay una tarjeta de video Nvidia, pero no está instalada en la máquina en la que se supone que se inician las aplicaciones CUDA. Para aquellos que estén interesados, bienvenidos a cat.
TLDRrCUDA (CUDA remota): un marco que implementa la API de CUDA, lo que le permite utilizar una tarjeta de video remota. Está en una versión beta funcional, disponible solo bajo Linux. El objetivo principal de rCUDA es la compatibilidad total con la API de CUDA, no necesita modificar su código de ninguna manera, solo configure variables de entorno especiales.
¿Qué es rCUDA?
rCUDA (Remote CUDA) es un marco que implementa la API de CUDA, lo que le permite utilizar una tarjeta de video ubicada en la máquina remota para la computación CUDA sin realizar ningún cambio en su código. Desarrollado en la Universidad Politécnica de Valencia ( equipo rcuda ).
Limitaciones
Actualmente solo se admiten sistemas GNU / Linux, sin embargo, los desarrolladores prometen compatibilidad con Windows en el futuro. La versión actual de rCUDA, 18.03beta, es compatible con CUDA 5-8, es decir, CUDA 9 no es compatible. Los desarrolladores declararon compatibilidad total con la API de CUDA, con la excepción de los gráficos.
Posibles casos de uso
- Ejecutar aplicaciones CUDA en una máquina virtual cuando reenvía una tarjeta de video es inconveniente o imposible, por ejemplo, cuando la tarjeta de video está ocupada por un host o cuando hay más de una máquina virtual.
- Portátil sin una tarjeta gráfica discreta.
- El deseo de usar múltiples tarjetas de video (agrupamiento). Teóricamente, puede usar todas las tarjetas de video disponibles en el equipo, incluso en forma conjunta.
Instrucciones breves
Configuración de prueba
Las pruebas se llevaron a cabo en la siguiente configuración:
Servidor:
Ubuntu 16.04, GeForce GTX 660
Cliente:
Una máquina virtual con Ubuntu 16.04 en una computadora portátil sin una tarjeta gráfica discreta.
Obteniendo rCUDA
La etapa más difícil. Desafortunadamente, en este momento, la única forma de obtener su copia de este marco es completar el formulario de solicitud correspondiente en el sitio web oficial. Sin embargo, los desarrolladores prometen responder dentro de 1-2 días. En mi caso, me enviaron una distribución el mismo día.
Instalar CUDA
Primero debe instalar el kit de herramientas CUDA en el servidor y el cliente (incluso si el cliente no tiene una tarjeta de video nvidia). Para hacer esto, puede descargarlo desde el sitio oficial o usar el repositorio. Lo principal es utilizar una versión no superior a 8. En este ejemplo, se utiliza el instalador .run del sitio oficial .
chmod +x cuda_8.0.61_375.26_linux.run ./cuda_8.0.61_375.26_linux.run
Importante! En el cliente, debe negarse a instalar el controlador nvidia. Por defecto, el Kit de herramientas de CUDA estará disponible en / usr / local / cuda /. Instale muestras de CUDA, las necesitará.
Instalar rCUDA
Descomprimiremos el archivo recibido de los desarrolladores en nuestro directorio de inicio en el servidor y en el cliente.
tar -xvf rCUDA*.tgz -C ~/ mv ~/rCUDA* ~/rCUDA
Debe realizar estas acciones tanto en el servidor como en el cliente.
Inicio del demonio rCUDA en el servidor
export PATH=$PATH/usr/local/cuda/bin export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64:/home/<XXX>/rCUDA/lib/cudnn cd ~/rCUDA/bin ./rCUDAd
Reemplace <XXX> con su nombre de usuario. Use ./rCUDAd -iv si desea ver resultados detallados.
Configuración del cliente
Abramos el terminal en el cliente, en el que ejecutaremos el código CUDA en el futuro. En el lado del cliente, necesitamos "reemplazar" las bibliotecas CUDA estándar con las bibliotecas rCUDA, para lo cual agregamos las rutas apropiadas a la variable de entorno LD_LIBRARY_PATH. También necesitamos especificar el número de servidores y sus direcciones (en mi ejemplo, será uno).
export PATH=$PATH/usr/local/cuda/bin export LD_LIBRARY_PATH=/home/<XXX>/rCUDA/lib/:$LD_LIBRARY_PATH export RCUDA_DEVICE_COUNT=1
Montaje y lanzamiento
Intentemos construir y ejecutar algunos ejemplos.
Ejemplo 1
Comencemos con un ejemplo simple de deviceQuery que simplemente muestra la configuración de CUDA para un dispositivo compatible, es decir, en nuestro caso, el GTX660 remoto.
cd <YYY>/NVIDIA_CUDA-8.0_Samples/1_Utilities/deviceQuery make EXTRA_NVCCFLAGS=--cudart=shared
Importante! Sin EXTRA_NVCCFLAGS = - cudart = compartido, el milagro no funcionará
Reemplace <YYY> con la ruta que especificó para las muestras CUDA al instalar CUDA.
Ejecute el ejemplo ensamblado:
./deviceQuery
Si hiciste todo correctamente, el resultado será algo como esto:
Resultado ./deviceQuery Starting... CUDA Device Query (Runtime API) version (CUDART static linking) Detected 1 CUDA Capable device(s) Device 0: "GeForce GTX 660" CUDA Driver Version / Runtime Version 9.0 / 8.0 CUDA Capability Major/Minor version number: 3.0 Total amount of global memory: 1994 MBytes (2090991616 bytes) ( 5) Multiprocessors, (192) CUDA Cores/MP: 960 CUDA Cores GPU Max Clock rate: 1072 MHz (1.07 GHz) Memory Clock rate: 3004 Mhz Memory Bus Width: 192-bit L2 Cache Size: 393216 bytes Maximum Texture Dimension Size (x,y,z) 1D=(65536), 2D=(65536, 65536), 3D=(4096, 4096, 4096) Maximum Layered 1D Texture Size, (num) layers 1D=(16384), 2048 layers Maximum Layered 2D Texture Size, (num) layers 2D=(16384, 16384), 2048 layers Total amount of constant memory: 65536 bytes Total amount of shared memory per block: 49152 bytes Total number of registers available per block: 65536 Warp size: 32 Maximum number of threads per multiprocessor: 2048 Maximum number of threads per block: 1024 Max dimension size of a thread block (x,y,z): (1024, 1024, 64) Max dimension size of a grid size (x,y,z): (2147483647, 65535, 65535) Maximum memory pitch: 2147483647 bytes Texture alignment: 512 bytes Concurrent copy and kernel execution: Yes with 1 copy engine(s) Run time limit on kernels: Yes Integrated GPU sharing Host Memory: No Support host page-locked memory mapping: Yes Alignment requirement for Surfaces: Yes Device has ECC support: Disabled Device supports Unified Addressing (UVA): Yes Device PCI Domain ID / Bus ID / location ID: 0 / 1 / 0 Compute Mode: < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) > deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 9.0, CUDA Runtime Version = 8.0, NumDevs = 1, Device0 = GeForce GTX 660 Result = PASS
Lo más importante que deberíamos ver:
Dispositivo 0 = GeForce GTX 660
Resultado = PASA
Genial Logramos construir y ejecutar la aplicación CUDA en una máquina sin una tarjeta gráfica discreta, utilizando para este propósito una tarjeta de video instalada en un servidor remoto.
Importante! Si el resultado de la aplicación comienza con líneas del formulario:
mlock error: Cannot allocate memory rCUDA warning: 1007.461 mlock error: Cannot allocate memory
significa que es necesario agregar las siguientes líneas al archivo "/etc/security/limits.conf" en el servidor y en el cliente:
* hard memlock unlimited * soft memlock unlimited
Por lo tanto, permitirá a todos los usuarios (*) memoria de bloqueo ilimitada (ilimitada) (memlock). Sería aún mejor reemplazar * con el usuario deseado, y en lugar de elegir de forma ilimitada los derechos menos gordos.
Ejemplo 2
Ahora intentemos algo más interesante. Probamos la implementación del producto escalar de vectores usando memoria compartida y sincronización ("Tecnología CUDA en Ejemplos" Sanders J. Kendrot E. 5.3.1).
En este ejemplo, calculamos el producto escalar de dos vectores de dimensión 33 * 1024, comparando la respuesta con el resultado obtenido en la CPU.
dotProd.cu #include <stdio.h> #define imin(a,b) (a<b?a:b) const int N = 33 * 1024; const int threadsPerBlock = 256; const int blocksPerGrid = imin(32, (N+threadsPerBlock-1) / threadsPerBlock); __global__ void dot(float* a, float* b, float* c) { __shared__ float cache[threadsPerBlock]; int tid = threadIdx.x + blockIdx.x * blockDim.x; int cacheIndex = threadIdx.x; float temp = 0; while (tid < N){ temp += a[tid] * b[tid]; tid += blockDim.x * gridDim.x; } // set the cache values cache[cacheIndex] = temp; // synchronize threads in this block __syncthreads(); // for reductions, threadsPerBlock must be a power of 2 // because of the following code int i = blockDim.x/2; while (i != 0){ if (cacheIndex < i) cache[cacheIndex] += cache[cacheIndex + i]; __syncthreads(); i /= 2; } if (cacheIndex == 0) c[blockIdx.x] = cache[0]; } int main (void) { float *a, *b, c, *partial_c; float *dev_a, *dev_b, *dev_partial_c; // allocate memory on the cpu side a = (float*)malloc(N*sizeof(float)); b = (float*)malloc(N*sizeof(float)); partial_c = (float*)malloc(blocksPerGrid*sizeof(float)); // allocate the memory on the gpu cudaMalloc((void**)&dev_a, N*sizeof(float)); cudaMalloc((void**)&dev_b, N*sizeof(float)); cudaMalloc((void**)&dev_partial_c, blocksPerGrid*sizeof(float)); // fill in the host memory with data for(int i=0; i<N; i++) { a[i] = i; b[i] = i*2; } // copy the arrays 'a' and 'b' to the gpu cudaMemcpy(dev_a, a, N*sizeof(float), cudaMemcpyHostToDevice); cudaMemcpy(dev_b, b, N*sizeof(float), cudaMemcpyHostToDevice); dot<<<blocksPerGrid, threadsPerBlock>>>(dev_a, dev_b, dev_partial_c); // copy the array 'c' back from the gpu to the cpu cudaMemcpy(partial_c,dev_partial_c, blocksPerGrid*sizeof(float), cudaMemcpyDeviceToHost); // finish up on the cpu side c = 0; for(int i=0; i<blocksPerGrid; i++) { c += partial_c[i]; } #define sum_squares(x) (x*(x+1)*(2*x+1)/6) printf("GPU - %.6g \nCPU - %.6g\n", c, 2*sum_squares((float)(N-1))); // free memory on the gpu side cudaFree(dev_a); cudaFree(dev_b); cudaFree(dev_partial_c); // free memory on the cpu side free(a); free(b); free(partial_c); }
Construye y ejecuta:
/usr/local/cuda/bin/nvcc --cudart=shared dotProd.cu -o dotProd ./dotProd
Este resultado nos dice que todo está bien con nosotros:
GPU - 2.57236e + 13
CPU - 2.57236e + 13
Ejemplo 3
Ejecute otra prueba estándar CUDA-matrixMulCUBLAS (multiplicación de matriz).
cd < YYY>/NVIDIA_CUDA-8.0_Samples/0_Simple/matrixMulCUBLAS make EXTRA_NVCCFLAGS=--cudart=shared ./matrixMulCUBLAS
Resultado[Matrix Multiply CUBLAS] - Comenzando ...
Dispositivo GPU 0: "GeForce GTX 660" con capacidad de cálculo 3.0
Matriz A (640,480), Matriz B (480,320), Matriz C (640,320)
Resultado de la computación usando CUBLAS ... hecho.
Rendimiento = 436.24 GFlop / s, Tiempo = 0.451 mseg, Tamaño = 196608000 Ops
Resultado de la computación utilizando la CPU del host ... hecho
Comparación de CUBLAS Matrix Multiply con resultados de CPU: PASS
NOTA: Las muestras CUDA no están destinadas a mediciones de rendimiento. Los resultados pueden variar cuando GPU Boost está habilitado.
Interesante para nosotros:
Rendimiento = 436.24 GFlop / s,
Comparación de CUBLAS Matrix Multiply con resultados de CPU: PASS
Seguridad
No encontré mención de ningún método de autorización en la documentación de rCUDA. Creo que en este momento lo más simple que se puede hacer es abrir el acceso al puerto deseado (8308) solo desde una dirección específica.
Usando iptables, se verá así:
iptables -A INPUT -m state --state NEW -p tcp -s < > --dport 8308 -j ACCEPT
Por lo demás, dejo el problema de seguridad más allá del alcance de esta publicación.
Fuentes y enlaces[1] http://www.rcuda.net/pub/rCUDA_guide.pdf
[2] http://www.rcuda.net/pub/rCUDA_QSG.pdf
[3] C. Reaño, F. Silla, G. Shainer y S. Schultz, "Las GPU locales y remotas funcionan de manera similar con EDR 100G InfiniBand", en las actas de la Conferencia Internacional de Middleware, Vancouver, BC, Canadá, diciembre de 2015.
[4] C. Reaño y F. Silla, "Una comparación de rendimiento de los marcos de virtualización de GPU remota CUDA", en los procedimientos de la Conferencia Internacional sobre Computación en Clúster, Chicago, IL, EE. UU., Septiembre de 2015.