
No hace mucho tiempo hablamos sobre el nuevo servicio Selectel:
computación en la nube de alto rendimiento en aceleradores FPGA . En un nuevo artículo sobre este tema, consideramos un ejemplo de programación FPGA para construir un conjunto de Mandelbrot, un algoritmo matemático conocido para visualizar imágenes fractales. El artículo utilizó material del sitio
Euler Project .
En lugar del prólogo
Primero, algunos términos. Un sistema informático con un acelerador FPGA: como regla, este es un adaptador PCIe con un chip FPGA como parte del servidor x64. El acelerador asume una tarea separada de uso intensivo de recursos en la que puede participar la computación paralela y lo realiza muchos órdenes de magnitud más rápido que el procesador x64, descargándolo y aumentando el rendimiento de todo el sistema informático. Por ejemplo, se puede realizar un ciclo de cálculo con 100 mil repeticiones en un FPGA en una sola pasada, en lugar de ejecutar secuencialmente 100 mil veces en un procesador clásico x64. El usuario programa directamente los elementos lógicos, los recursos de hardware, los enlaces de comunicación, los chips FPGA para la tarea en sí, lo que le permite implementar la tarea como una implementación de un algoritmo en silicio: Algoritmo en silicio y, por lo tanto, lograr un alto rendimiento y un consumo de energía muy modesto.
Hoy en día, el umbral para ingresar a la tecnología FPGA es bastante accesible incluso para las nuevas empresas: un servidor con un acelerador FPGA y todo el software necesario (SDK) se puede alquilar en la nube Selectel por un precio razonable (el llamado "FPGA en la nube"), y el soporte para el estándar Open CL en FPGA conduce a que un programador que sabe trabajar con C es capaz de preparar y ejecutar un programa en FPGA.
Mirando hacia el futuro: pruebe FPGA en el trabajo
El ejemplo de programación descrito a continuación para construir un conjunto de Mandelbrot ya se ha implementado en un servidor de prueba
en el Laboratorio Selectel , donde cualquiera puede evaluar su rendimiento (se requerirá el registro).
El proyecto se proporciona en código y está preparado para su compilación. Selectel ofrece acceso remoto a un servidor con acelerador Intel Arria 10 FPGA. En el lado del servidor, se implementan las herramientas SDK y BSP para desarrollar, depurar y compilar OpenCL, el código de Visual Studio para preparar aplicaciones host (aplicaciones de control para el procesador central del servidor).
Tenga en cuenta que el ejemplo en sí no tiene ningún valor aplicado; fue elegido por razones de demostración de los métodos de aceleración utilizando los principios del paralelismo. Con este ejemplo, el lector se familiariza con la ruta de diseño de una aplicación en un sistema informático heterogéneo con FPGA; más tarde, esta ruta se puede utilizar para desarrollar sus propias aplicaciones con la informática paralela.
ACTUALIZACIÓN : En la primavera de 2018, Intel presentó el procesador híbrido de alto rendimiento Xeon Gold 6138P con un chip Arria 10 FPGA integrado. Para fines de 2018, se espera que los procesadores en serie de este tipo estén disponibles para los clientes a través de socios de Intel. En Selectel esperamos este chip, y esperamos ser los primeros en Rusia en brindar a nuestros clientes la oportunidad de probar este nuevo producto único.
Acerca del estándar OpenCL para programación FPGA
El estándar OpenCL fue desarrollado por Khronos Group, los principales fabricantes mundiales de chips y software que incluyen Intel, AMD, Apple, ARM, Nvidia, Sony Computer Entertainment y otros, y está diseñado para escribir aplicaciones que usan computación paralela en varios tipos de procesadores, incluido FPGA. El estándar OpenCL incluye el lenguaje de programación C basado en la versión del lenguaje C99 (la última versión de C99 es ISO / IEC 9899: 1999 / Cor 3: 2007 del 15-11-2007) y un entorno de programación de aplicaciones.
La popularidad del uso de OpenCL para la informática de alto rendimiento se basa en el hecho de que es un estándar abierto y su uso no requiere una licencia. Además, OpenCL no limita el rango de dispositivos compatibles a ninguna marca en particular, permitiendo el uso de hardware de diferentes fabricantes en la misma plataforma de software.
Además sobre OpenCL: Introducción a OpenCL en Habr .
Un poco de historia: la ruta de diseño de FPGA que existía antes del estándar OpenCL era extremadamente específica y lenta, mientras que en términos de complejidad era incluso superior al diseño de chip personalizado (ASIC, circuito integrado específico de la aplicación, "circuito integrado de propósito especial"). Se requería una comprensión profunda de la estructura del hardware FPGA, cuya configuración debía llevarse a cabo en un lenguaje de descripción de hardware (HDL) de bajo nivel. La posesión de esta ruta de diseño y verificación ha sido y sigue siendo un arte que, debido a la extrema complejidad, está disponible para un círculo limitado de desarrolladores.
La llegada del kit de herramientas de soporte OpenCL de Intel para FPGA ha abordado en parte el problema de la accesibilidad de la programación FPGA para desarrolladores de software. El programador selecciona independientemente la parte de su algoritmo que es adecuada para el procesamiento en paralelo y la describe en C, luego el compilador Intel OpenCL para FPGA crea un archivo de configuración binario para ejecutar este fragmento del algoritmo en el acelerador.
Utilizando el entorno habitual de Visual Studio o el compilador gcc estándar, se prepara una aplicación host (una aplicación del tipo .exe, ejecutada en el procesador x64 principal), mientras que todas las bibliotecas de soporte necesarias están incluidas en el SDK. Cuando se inicia la aplicación host, se carga el firmware FPGA, los datos se cargarán en el núcleo del chip y el procesamiento comenzará de acuerdo con el algoritmo concebido.
FPGA (FPGA) es una estructura de hardware masivamente paralela reprogramable por el usuario con millones de elementos lógicos, miles de unidades de señal DSP y decenas de megabytes de caché para cálculos integrados, sin acceder a los módulos de memoria principales del servidor. Las interfaces de E / S rápidas (10GE, 40GE, 100GE, PCIe Gen 3, etc.) le permiten intercambiar datos de manera efectiva con el procesador principal del servidor.
El estándar OpenCL es un entorno para ejecutar software heterogéneo. El entorno consta de dos partes separadas:
- Software de host: una aplicación que se ejecuta en el procesador central principal del servidor, escrita en C / C ++ y que utiliza el conjunto de funciones API de OpenCL. El servidor host organiza todo el proceso de computación, suministra la fuente y recibe datos de salida, e interactúa con todos los sistemas del servidor con el acelerador FPGA.
- Software acelerador: un programa escrito en el lenguaje OpenCL C (lenguaje C con varias restricciones), compilado para ejecutarse en el chip FPGA.
Un servidor típico para la computación paralela es una computadora basada en x64 (para ejecutar aplicaciones host), que incluye un acelerador de hardware FPGA, conectado con mayor frecuencia a través del bus PCI-Express. Por cierto, tal sistema se presenta en el Laboratorio Selectel.
La secuencia de programación y compilación para el acelerador FPGA consta de dos etapas. El compilador estándar compila el código de la aplicación host (Visual C ++, GCC) para obtener un archivo ejecutable en el sistema operativo del servidor (por ejemplo, * .exe). El compilador de AOC prepara el código fuente del acelerador FPGA (kernel, kernel) como parte del SDK, con la recepción de un archivo binario (* .aocx). Este archivo es solo para la programación del acelerador.

Fig. Arquitectura del entorno de compilación de software OpenCL
Considere algún código de ejemplo para calcular un vector grande de dos maneras
(
PD: No dispares al pianista; en lo sucesivo, se usa el código del sitio del Proyecto Euler ):
void inc (float *a, float c, int N) { for (int i = 0; i<N; i++) a[i] = a[i] + c; } void main() { ... inc(a,c,N); ... }
_kernel void inc (_global float *a, float c) { int i = get_global_id(0); a[i] = a[i] + c; } void main() { ... clEnqueueNDRangeKernel(...,&N,...) ... }
El código al principio es un ejemplo de cómo se vería una implementación de subproceso único en C utilizando el método de cálculo secuencial de elementos escalares.
La segunda versión del código es una posible implementación del algoritmo en OpenCL en forma de una función calculada en un acelerador FPGA. No hay bucle, y el cálculo se realiza en una iteración del bucle. El cálculo de una matriz de vectores ocurre como la ejecución de N copias de esta función. Cada copia tiene su propio índice, sustituido en el iterador en un bucle, y el número de reintentos se establece desde el host cuando se ejecuta el código. La acción de iterador es proporcionada por la función get_global_id (), que funciona con un índice dentro de 0 ≤ índice <N.
Ve al grano: construyendo un fractal
El conjunto de Mandelbrot es una matriz de puntos "c" en el plano complejo para el cual la relación de recurrencia Zn + 1 = Zn² + c para Z0 = 0 define una secuencia acotada.
Definimos Zn = Zn + IYn, y también c = p + iq.
Para cada punto, se calcula la siguiente secuencia:
Xn + 1 = Xn² + Yn² + p
Yn + 1 = 2XnYn + q
El cálculo de la pertenencia de un punto al conjunto en cada iteración se realiza como la ecuación
Xn² + Yn² <4.
Para mostrar el conjunto de Mandelbrot en la pantalla, definimos una regla:
- Si la desigualdad se mantiene en cualquier iteración, el punto ingresa al conjunto y se mostrará en negro.
- Si la desigualdad no se cumple, comenzando con un cierto valor de iteración n = N, entonces el color está determinado por el número de iteraciones N.
El proceso de cálculo en el host será el siguiente:
- El cálculo del número de iteraciones para cada punto dentro de la ventana de píxeles se asigna a la función mandel_pixel ().
- La enumeración secuencial de puntos de imagen será proporcionada por la función softwareCalculateFrame (). Los parámetros especifican el intervalo real de los puntos calculados, el paso real del algoritmo y un puntero al búfer de color del tamaño de la imagen (theWidth * theHeight).
- El color del punto se ajusta mediante la tabla de colores suaves.
Pasemos al código:
inline unsigned int mandel_pixel( double x0, double y0, unsigned int maxIterations ) { // variables for the calculation double x = 0.0; double y = 0.0; double xSqr = 0.0; double ySqr = 0.0; unsigned int iterations = 0; // perform up to the maximum number of iterations to solve // the current point in the image while ( xSqr + ySqr < 4.0 &&iterations < maxIterations ) { // perform the current iteration xSqr = x*x; ySqr = y*y; y = 2*x*y + y0; x = xSqr - ySqr + x0; // increment iteration count iterations++; } // return the iteration count return iterations; }
int softwareCalculateFrame( double aStartX, double aStartY, double aScale, unsigned int* aFrameBuffer ) { // temporary pointer and index variables unsigned int * fb_ptr = aFrameBuffer; unsigned int j, k, pixel; // window position variables double x = aStartX; double y = aStartY; double cur_x, cur_y; double cur_step_size = aScale; // for each pixel in the y dimension window for ( j = 0, cur_y = y; j < theHeight; j++, cur_y -= cur_step_size ) { // for each pixel in the x dimension of the window for ( cur_x = x, k = 0; k< theWidth; k++, cur_x += cur_step_size ) { // set the value of the pixel in the window pixel = mandel_pixel(cur_x, cur_y, theSoftColorTableSize); if ( pixel == theSoftColorTableSize ) *fb_ptr++ = 0x0; else *fb_ptr++ = theSoftColorTable[pixel]; } } return 0; }
Cada píxel se calcula independientemente del otro y, por lo tanto, este proceso puede ser paralelo. Al implementar el algoritmo para el acelerador FPGA, se crea una instrucción SIMD para calcular el número de cada píxel de iteración (determinando el código de color de la paleta). La implementación de dos bucles anidados en el búfer de imagen se enmarca a través de OpenCL ejecutando la operación (theWidth * theHeight).
Las instancias del núcleo en la lista a continuación se denominan elementos de trabajo, y el conjunto de todas las instancias se denomina espacio de índice. Las características de la función de hardware incluyen lo siguiente:
- Una declaración de función comienza con la palabra clave __kernel.
- Tipo de función de hardware: el tipo del valor de retorno siempre es nulo.
- La devolución de valores se realiza a través de buffers pasados como parámetros.
- Los primeros tres parámetros definen la cuadrícula de material, cuyos nodos corresponden a los píxeles de la imagen de salida.
- El cuarto parámetro limita el número de iteraciones, evitando el bucle de puntos que pertenecen al conjunto de Mandelbrot.
- El quinto parámetro es un puntero al búfer de color de salida.
- La palabra clave __global denota el tipo de memoria a través de la cual se transmitirá el búfer: esta es la memoria general DDR (QDR) en el acelerador.
- La palabra clave restrictiva prohíbe al optimizador el uso de referencias indirectas de almacenamiento intermedio.
- En el sexto parámetro, se pasa un puntero a la paleta.
- La palabra clave __constant optimiza los accesos al búfer al generar un caché con un atributo de solo lectura.
La descripción de la función en el listado está cerca de la implementación para el procesador x64. Aquí, la definición de la instancia actual del núcleo se realiza a través de la función get_global_id, en la que se pasa el número de dimensión (0, 1) como parámetro.
Para una mejor optimización, se introduce una indicación explícita del inicio del ciclo. En ausencia de información sobre el número de iteraciones en el momento de la compilación, el número de pasos de bucle se indica explícitamente, ya que se crearán sus propios bloques de hardware para ellos. Con este tipo de codificación, uno debe "mirar hacia atrás" a la capacidad de un chip específico instalado en el acelerador, debido al consumo de recursos FPGA durante un mayor número de ciclos.
//////////////////////////////////////////////////////////////////// // mandelbrot_kernel.cl : Hardware implementation of the mandelbrot algorithm //////////////////////////////////////////////////////////////////// // Amount of loop unrolling. #ifndef UNROLL #define UNROLL 20 #endif // Define the color black as 0 #define BLACK 0x00000000 __kernel void hw_mandelbrot_frame ( const double x0, const double y0, const double stepSize, const unsigned int maxIterations, __global unsigned int *restrict framebuffer, __constant const unsigned int *restrict colorLUT, const unsigned int windowWidth) { // Work-item position const size_t windowPosX = get_global_id(0); const size_t windowPosY = get_global_id(1); const double stepPosX = x0 + (windowPosX * stepSize); const double stepPosY = y0 - (windowPosY * stepSize); // Variables for the calculation double x = 0.0; double y = 0.0; double xSqr = 0.0; double ySqr = 0.0;</code> <code>unsigned
El paquete de utilidades Intel FPGA SDK para OpenCL deberá instalarse en el host antes de compilar la implementación de hardware del algoritmo. Entre el software preinstalado, debe incluir BSP (Board Support Package) del fabricante de la placa aceleradora específica. En el ejemplo, Intel Quartus Prime Pro 16.1 se instala con soporte para OpenCL y BSP del acelerador Euler Thread (Intel Arria 10).
La siguiente es la configuración de rutas y variables de entorno. La variable ALTERAOCLSDKROOT contiene la ruta al SDK Intel FPGA, la variable AOCL_BOARD_PACKAGE_ROOT contiene la ruta al acelerador BSP.
set ALTERAOCLSDKROOT=C:\intelFPGA_pro\16.1\hld set AOCL_BOARD_PACKAGE_ROOT=C:\intelFPGA_pro\16.1\hld\board\euler_thread set path=%path%;C:\intelFPGA_pro\16.1\hld\bin set path=%path%;C:\intelFPGA_pro\16.1\quartus\bin64 set path=%path%;C:\intelFPGA_pro\16.1\hld\board\a10_ref\windows64\bin set path=%path%;C:\intelFPGA_pro\16.1\hld\host\windows64\bin set path=%path%;C:\intelFPGA_pro\16.1\qsys\bin set path=%path%;C:\Program Files (x86)\GnuWin32\bin\
Para la compilación, se utiliza el compilador aoc del SDK.
aoc mandelbrot_kernel.cl -o mandelbrot_kernel.aocx --board thread -v -v --report
Desciframos: mandelbrot_kernel.cl - el archivo con el texto fuente, mandelbrot_kernel.aocx - el archivo objeto de salida para programar FPGA, thread - el nombre del acelerador del paquete BSP. El modificador --report muestra un informe de uso de recursos FPGA. El modificador –v muestra información de diagnóstico durante la compilación. El informe de consumo de recursos para el núcleo es el siguiente:
+ ------------------------------------------------- ------------------- +
; Resumen de uso estimado de recursos;
+ ---------------------------------------- + -------- ------------------- +
; Recurso + Uso;
+ ---------------------------------------- + -------- ------------------- +
; Utilización de la lógica; 49%
; ALUTs; 26%;
; Registros lógicos dedicados; 25%;
; Bloques de memoria; 21%
; Bloques DSP; 16%;
+ ---------------------------------------- + -------- -------------------;
Para compilar la aplicación host, el ejemplo utilizó el paquete Microsoft Visual Studio 2010 Express con Microsoft SDK 7.1 instalado. En la configuración del proyecto, se selecciona la configuración para x64. A continuación, conecte la carpeta para los archivos de encabezado externos y especifique la ruta a las bibliotecas adicionales Intel FPGA SDK en la configuración del vinculador.
Directorios adicionales para incluir archivos = $ (ALTERAOCLSDKROOT) \ host \ include;
Directorios de biblioteca adicionales = $ (AOCL_BOARD_PACKAGE_ROOT) \ windows64 \ lib;
$(ALTERAOCLSDKROOT)\host\windows64\lib;
El plan de acción general para iniciar el kernel en el acelerador será el siguiente:
- Obtenga una lista de plataformas
- Obtenga una lista de dispositivos
- crear contexto;
- cargar el kernel en el dispositivo;
- enviar memorias intermedias de entrada al dispositivo;
- ejecutar el núcleo para su ejecución;
- lea el búfer de salida del dispositivo;
- contexto libre
Considere algunos puntos directamente relacionados con el lanzamiento del kernel. Entonces, un núcleo está diseñado para procesar un píxel de la imagen. Por lo tanto, debe ejecutar N instancias de kernel, donde N es el número total de píxeles en la imagen.
A continuación, observamos el caso cuando hay varias placas aceleradoras en el servidor, entonces la tarea se puede distribuir entre ellas. En cada uno de los aceleradores, debe cargar el kernel (archivo mandelbrot_kernel.aocx). Suponga que el número de aceleradores es numDevices, y las líneas de imagen se dividen entre todos los aceleradores:
#define MAXDEV 10 static cl_context theContext; static cl_program theProgram; static cl_kernel theKernels[MAXDEV]; //.. // Create the program object theProgram = createProgramFromBinary( theContext, "mandelbrot_kernel.aocx", theDevices, numDevices); // Create the kernels for ( unsigned i = 0; i < numDevices; ++i ) theKernels[i] = clCreateKernel( theProgram, "hw_mandelbrot_frame", &theStatus ); // Create output pixel buffers for every kernel for( unsigned i = 0; i < numDevices; ++i ) thePixelData[i] = clCreateBuffer(theContext, CL_MEM_WRITE_ONLY, thePixelDataWidth*rowsPerDevice[i]*sizeof(unsigned int), NULL, &theStatus); // Preparing and writing palette buffer to every device theHardColorTable = clCreateBuffer(theContext, CL_MEM_READ_ONLY, aColorTableSize*sizeof(unsigned int), NULL, &theStatus); for( unsigned i = 0; i < numDevices; i++ ) theStatus = clEnqueueWriteBuffer(theQueues[i], theHardColorTable, CL_TRUE, 0, aColorTableSize*sizeof(unsigned int), aColorTable, 0, NULL, NULL); // Preparing kernels and run unsigned rowOffset = 0; for ( unsigned i = 0; i < numDevices; rowOffset += rowsPerDevice[i++] ) { // Create ND range size size_t globalSize[2] = { thePixelDataWidth, rowsPerDevice[i] }; // Set the arguments unsigned argi = 0; theStatus = clSetKernelArg (theKernels[i], argi++, sizeof(cl_double), (void*) &aStartX ); const double offsetedStartY = aStartY - rowOffset * aScale; theStatus = clSetKernelArg(theKernels[i], argi++, sizeof(cl_double), (void*)&offsetedStartY); theStatus = clSetKernelArg(theKernels[i], argi++, sizeof(cl_double), (void*)&aScale); theStatus = clSetKernelArg(theKernels[i], argi++, sizeof(cl_uint), (void*)&theHardColorTableSize); theStatus = clSetKernelArg(theKernels[i], argi++, sizeof(cl_mem), (void*)&thePixelData[i]); theStatus = clSetKernelArg(theKernels[i], argi++, sizeof(cl_mem), (void*)&theHardColorTable); theStatus = clSetKernelArg(theKernels[i], argi++, sizeof(cl_uint), (void*)&theWidth); // Launch kernel theStatus = clEnqueueNDRangeKernel(theQueues[i], theKernels[i], 2, NULL, globalSize, NULL, 0, NULL, NULL); } rowOffset = 0; for( unsigned i = 0; i < numDevices; rowOffset += rowsPerDevice[i++] ) { // Read the output theStatus = clEnqueueReadBuffer(theQueues[i], thePixelData[i], CL_TRUE, 0, thePixelDataWidth*rowsPerDevice[i]*sizeof(unsigned int), &aFrameBuffer[rowOffset * theWidth], 0, NULL, NULL); } / / . .
- La función createProgramFromBinary crea un objeto de programa OpenCL a partir de un archivo de objeto.
- A continuación, para cada dispositivo, se crea un núcleo basado en el objeto del programa.
- Los almacenamientos intermedios de PixelData se crean para recibir resultados de cada núcleo.
- Se crea un búfer para almacenar la paleta de colores y se carga en cada uno de los aceleradores.
- A continuación, para cada dispositivo, el enlace de los parámetros de la aplicación local y los parámetros del kernel se establece mediante la función clSetKernelArg.
- Los parámetros están determinados por los números de serie en la declaración de la función del núcleo, comenzando desde cero.
El siguiente punto importante es determinar el tamaño de la tarea en función del espacio de índice de acuerdo con la matriz globalSize. Esta matriz puede ser de una, dos o tres dimensiones. Para cada dimensión, una dimensión se da como un número entero. La dimensión del espacio determinará el orden de índice del elemento de trabajo en el núcleo.
En el ejemplo, para cada núcleo, se especifica un espacio bidimensional, donde uno de los ejes son los elementos de la fila de píxeles, el segundo es el conjunto de líneas de imagen procesadas en este dispositivo. En el código del núcleo, el número de píxel en la línea se obtiene llamando a get_global_id (0), el número de línea es get_global_id (1). La variable globalSize se pasa a la función clEnqueueNDRangeKernel para iniciar el número requerido de instancias del núcleo para ejecutar.
Una vez completada la ejecución de los núcleos, las memorias intermedias de píxeles se leen desde el dispositivo a las matrices locales. Evaluemos el rendimiento por la cantidad de fotogramas por segundo: el resultado es visible en la demostración realizada en la conferencia SelectelTechDay ( consulte el comienzo del artículo ).
Conclusión
La programación de aceleradores FPGA en un lenguaje de alto nivel indudablemente redujo el umbral de acceso a esta tecnología para los desarrolladores en un orden de magnitud. Por ejemplo, para aquellos que solo están dominando este kit de herramientas, incluso hay una implementación FPGA del famoso ejemplo "Hello World" .
Pero no tan simple. Escribir, y especialmente, depurar un algoritmo claramente funcional de un problema real aplicado todavía requiere una gran profesionalidad. Otra limitación es que cada chip FPGA puede realizar solo una tarea computacional dentro de la aplicación. Para otra tarea, debe reprogramarse nuevamente.
Por cierto, el modelo de uso de la plataforma le permite tener más de un acelerador FPGA en el host, aunque esta es una solución bastante costosa.
El host (aplicación host) gestiona el proceso de creación del contexto (estructura de datos para el acelerador) y la cola de comandos. Es decir Una sola aplicación host, en la que hay varias subtareas para computación paralela en FPGA, puede cargarlas en diferentes aceleradores:
KERNEL1 => ACELERADOR A
KERNEL2 => ACELERADOR B
Sin embargo, los esfuerzos para dominar los aceleradores FPGA valen la pena: en muchas áreas de aplicación, esta tecnología se está volviendo indispensable: telecomunicaciones, biotecnología, procesamiento de grandes datos, reconocimiento de patrones, procesamiento de señales e imágenes, en matemática computacional y modelado de campo físico.
Información adicional para el artículo:
www.altera.com es el recurso principal de Intel FPGA.
www.eulerproject.com es el sitio web oficial del Proyecto Euler.
Altera + OpenCL: programamos bajo FPGA sin conocimiento de VHDL / Verilog - un artículo sobre Habr.