Buscar..


Introducción

Antes de utilizar OpenCL, uno tiene que configurar su código para usarlo. Este tema se enfoca en cómo hacer que Opencl se ejecute y ejecute en su proyecto y ejecute un núcleo básico. Los ejemplos se basan en el contenedor OpenCL.NET de C #, pero como el contenedor no agrega abstracción a OpenCL, el código probablemente se ejecutará con muy pocos cambios en C / C ++ también.

Las llamadas en C # pueden tener el siguiente aspecto: 'Cl.GetPlatformIDs'. Para el Api OpenCL de estilo C, se llamaría 'clGetPlatformIDs' y para el estilo C ++ one 'cl :: GetPlatformIDs'

Observaciones

  • NVidia, AMD e Intel tienen implementaciones ligeramente diferentes de OpenCL, pero las diferencias conocidas están (según mi experiencia) limitadas a los requisitos de paréntesis y las conversiones implícitas. A veces NVidia colapsará tu kernel mientras trata de averiguar la sobrecarga correcta para un método. En este caso, es útil ofrecer un reparto explícito para ayudar a la GPU. El problema fue observado para los núcleos compilados en tiempo de ejecución.

  • Para obtener más información sobre las llamadas utilizadas en este tema, basta con google 'OpenCL' seguido del nombre de la función. El grupo Khronos tiene una documentación completa sobre todos los parámetros y tipos de datos disponibles en su sitio web.

Inicializando el dispositivo de destino

Los núcleos OpenCL pueden ejecutarse en la GPU o en la CPU. Esto permite soluciones alternativas, donde el cliente puede tener un sistema muy obsoleto. El programador también puede elegir limitar su funcionalidad a la CPU o GPU.

Para comenzar a utilizar OpenCL, necesitará un 'Contexto' y un 'Dispositivo'. Ambas son estructuras definidas por la API de OpenCL (también conocida como cl :: Context o clContext & ~ Device) y definen el procesador de destino utilizado.

Para obtener su dispositivo y contexto, necesita consultar una lista de plataformas disponibles, cada una de las cuales puede alojar múltiples dispositivos. Una plataforma representa su GPU física y CPU, mientras que un dispositivo puede distinguir aún más las unidades informáticas contenidas. Para las GPU, la mayoría de las plataformas solo tendrán un dispositivo. Pero una CPU puede ofrecer una GPU integrada adicional junto a sus capacidades de CPU.

El contexto gestiona la memoria, las colas de comandos, los diferentes kernels y programas. Un contexto puede limitarse a un solo dispositivo, pero también hacer referencia a múltiples dispositivos.

Una nota rápida de la API antes de comenzar a codificar: Casi todas las llamadas a OpenCL le dan un valor de error, ya sea como valor de retorno o mediante un valor de referencia (puntero en C). Ahora vamos a empezar.

ErrorCode err;
var platforms = Cl.GetPlatformIDs(out err);
if(!CheckError(err, "Cl.GetPlatformIDs")) return;
foreach (var platform in platforms) {
    foreach (var device in Cl.GetDeviceIDs(platform, DeviceType.Gpu, out err)) {
        if(!CheckError(err, "Cl.GetDeviceIDs")) continue;
        [...]
    }
}

Este fragmento de código consulta todos los dispositivos GPU disponibles en el sistema. Ahora puede agregarlos a una lista o comenzar su contexto directamente con la primera coincidencia. La función 'CheckError (...)' es una utilidad simple, que verifica si el código de error tiene el valor de éxito o uno diferente y puede ofrecerle algún registro. Se recomienda utilizar una función o macro por separado, porque llamará mucho a eso.

ErrorCode es solo una enumeración en el tipo de datos cl_int para C #, C / C ++ puede comparar el valor int con constantes de error predefinidas como se muestra aquí: https://www.khronos.org/registry/OpenCL/sdk/1.0/docs/man/ xhtml / errors.html

También es posible que desee comprobar si el dispositivo admite todas las funciones necesarias, de lo contrario, sus núcleos podrían bloquearse en el tiempo de ejecución. Puede consultar una capacidad del dispositivo con

Cl.GetDeviceInfo(_device, DeviceInfo.ImageSupport, out err)

Este ejemplo le pregunta al dispositivo si puede ejecutar funciones de imagen. Para el siguiente y último paso, debemos construir nuestro contexto a partir de los dispositivos recopilados.

_context = Cl.CreateContext(null, 1, new[] { _device }, ContextNotify, IntPtr.Zero, out err);

Algunas cosas están pasando aquí. Para la gente de C / C ++, IntPtr es una dirección de puntero en C #. Me concentraré en las partes importantes aquí.

  • El segundo parámetro define la cantidad de dispositivos que desea usar
  • El tercer parámetro es una matriz de esos dispositivos (o un puntero en C / C ++)
  • Y el tercer parámetro es un puntero de función para una función de devolución de llamada. Esta función se utilizará siempre que ocurran errores dentro del contexto.

Para su uso futuro, deberá conservar los dispositivos usados ​​y el contexto en algún lugar.

Cuando hayas terminado toda tu interacción con OpenCL, necesitarás liberar el contexto nuevamente con

Cl.ReleaseContext(_context);

Compilando tu Kernel

Los kernels se pueden compilar en tiempo de ejecución en el dispositivo de destino. Para ello, necesitas

  • el código fuente del kernel
  • El dispositivo de destino en el que compilar
  • un contexto construido con el dispositivo de destino

Una actualización rápida de terminología: un programa contiene una colección de núcleos. Puede pensar en un programa como un archivo fuente completo de C / C ++ / C #, mientras que los núcleos son los diferentes miembros de la función de ese archivo.

Primero necesitarás crear un programa a partir de tu código fuente.

var program = Cl.CreateProgramWithSource(_context, 1, new[] { source }, null, out err);

Puede combinar varios archivos de origen en un solo programa y compilarlos juntos, lo que le permite tener núcleos en diferentes archivos y compilarlos juntos de una sola vez.

En el siguiente paso, deberá compilar el programa en su dispositivo de destino.

err = Cl.BuildProgram(program, 1, new[] { _device }, string.Empty, null, IntPtr.Zero);

Ahora viene una pequeña advertencia: el código de error solo le dice si la llamada a la función fue exitosa, pero no si su código realmente se compiló. Para verificar eso, tenemos que consultar alguna información adicional.

BuildStatus status;
status = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Status, out err).CastTo<BuildStatus>();
if (status != BuildStatus.Success) {
    var log = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Log, out err);
}

La gente de C / C ++ puede ignorar la conversión al final y simplemente comparar el entero devuelto con la constante correspondiente.

La primera llamada verifica si nuestra compilación fue realmente exitosa. Si no, podemos recuperar un registro y ver exactamente dónde salieron las cosas mal. Vea las observaciones de algunos pitfals comunes con respecto a diferentes plataformas.

Una vez que se construye el programa, necesitas extraer tus diferentes núcleos del programa compilado. Para ello creas tus kernels con

_kernel = Cl.CreateKernel(_program, kernel, out err);

donde 'kernel' es una cadena del nombre del kernel. Cuando haya terminado con su kernel, necesita liberarlo con

Cl.ReleaseKernel(_kernel);

Creando una cola de comandos

Para iniciar cualquier operación en sus dispositivos, necesitará una cola de comandos para cada dispositivo. La cola hace un seguimiento de las diferentes llamadas que hizo al dispositivo de destino y las mantiene en orden. La mayoría de los comandos también se pueden ejecutar en modo de bloqueo o no bloqueo.

Crear una cola es bastante sencillo:

_queue = Cl.CreateCommandQueue(_context, _device, CommandQueueProperties.None, out err);

La interacción básica con su cola de comandos es poner en cola las diferentes operaciones que desea realizar, por ejemplo, copiar datos desde y hacia su dispositivo e iniciar un kernel.

Cuando haya terminado de usar la cola de comandos, debe liberar la cola con una llamada a

Cl.ReleaseCommandQueue(_queue);

Ejecutando el Kernel

Así que ahora vamos a lo real, ejecutando sus núcleos en el dispositivo paralelo. Lea acerca de los conceptos básicos de hardware para comprender completamente el envío del kernel.

Primero deberá establecer los argumentos del kernel antes de llamar al kernel. Esto se hace a través de

err = Cl.SetKernelArg(_kernel, $argumentIndex, $argument);

Si no establece todos los argumentos antes de lanzar el kernel, el kernel fallará.

Antes de lanzar nuestro kernel, debemos calcular el 'tamaño de trabajo global' y el 'tamaño de trabajo local'.

el tamaño de trabajo global es el número total de subprocesos que se lanzarán en su GPU. El tamaño de trabajo local es el número de subprocesos dentro de cada bloque de subprocesos. El tamaño de la obra local se puede omitir si el núcleo no necesita ningún requisito especial. Pero si se especifica el tamaño del trabajo local, el tamaño del trabajo global debe ser un múltiplo del tamaño del trabajo local.

Los tamaños de trabajo pueden ser unidimensionales, bidimensionales o tridimensionales. La elección de cuántas dimensiones desea depende totalmente de usted y puede elegir la que mejor se adapte a su algoritmo.

Ahora que decidimos nuestros tamaños de trabajo, podemos llamar el núcleo.

Event clevent;
err = Cl.EnqueueNDRangeKernel(_queue, _kernel, $dimensions, null, $globalWorkSize, $localWorkSize, 0, null, out clevent);

Las $ dimensiones definen nuestro número deseado de dimensiones, $ globalWorkSize es un conjunto de dimensiones $ dimensiones con el tamaño de Trabajo global y lo mismo para $ localWorkSize. El último argumento le da un objeto que representa su kernel actualmente ejecutado.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow