[Высокопроизводительные вычисления] Синтаксис Opencl и связанные с ним понятия (2): индекс, очередь, функция ядра.

Параллелизм данных и параллелизм задач

Параллелизм данных делит крупномасштабные вычислительные задачи на несколько подзадач и одновременно применяет эти подзадачи к разным наборам данных. Каждая подзадача выполняется на независимом процессоре, что повышает производительность вычислений за счет параллельной обработки различных наборов данных. Параллелизм данных особенно подходит для ситуаций, когда одна и та же операция выполняется над крупномасштабными наборами данных, например умножение матриц, обработка изображений и т. д.

Параллелизм задач — это разделение вычислительных задач на несколько подзадач, при этом каждая подзадача выполняет различную последовательность операций или инструкций. Различные подзадачи выполняются одновременно на разных процессорах, что повышает производительность вычислений за счет параллельного выполнения нескольких различных операций. Параллелизм задач особенно подходит для сложных вычислительных задач, в которых существуют зависимости между различными подзадачами и для выполнения которых необходимо работать вместе.

Общие черты гетерогенных языков программирования

Вставьте сюда описание изображенияВ высокопроизводительных вычислениях «ядро» относится к наименьшему исполнительному блоку или наименьшему вычислительному блоку вычислительной задачи. Он представляет собой набор инструкций, которые могут быть выполнены процессором для выполнения определенного вычисления или операции.

Ядро обычно представляет собой реализацию, оптимизированную для конкретной вычислительной задачи или алгоритма. Он может содержать множество вычислений и операций обработки данных, таких как умножение матриц, векторные операции, алгоритмы сортировки и т. д. Ядро спроектировано так, чтобы полностью использовать аппаратные ресурсы (такие как процессоры, память и т. д.) для повышения производительности и эффективности вычислений за счет параллельного выполнения нескольких экземпляров ядра.

Ядро обычно работает независимо от основной программы и может выполняться параллельно на нескольких процессорах. Оптимизация ядра включает использование локальности данных, векторизованных инструкций, оптимизации кэша и других технологий для максимального использования вычислительной мощности процессора.

В высокопроизводительных вычислениях за счет рационального разделения и управления ядрами задач можно полностью использовать потенциал параллельных вычислений и повысить эффективность выполнения и общую производительность программы. Оптимизация ядра — ключевая часть высокопроизводительных вычислений.
Вставьте сюда описание изображения

метод разделения opencl

Вставьте сюда описание изображения
(1) Глобальный индекс: для всей квадратной матрицы, показанной на диаграмме, координата равна (6, 5).
(2) Рабочая группа (localSize) — это второе грубое разделение глобальных рабочих элементов.
(3) Локальный индекс — это индекс рабочего элемента в рабочей группе.
Используя рабочие группы в функции ядра, доступ к данным можно получить, получив глобальный и локальный индекс каждого рабочего элемента. Логика расчета и доступа к индексу следующая:

  1. Получить глобальный индекс. Используйте встроенные переменные, get_global_id(dim)чтобы получить dimглобальный индекс текущего рабочего элемента в измерении. dimЗначение обычно равно 0, что указывает на первое измерение.

  2. Получить локальный индекс. Используйте встроенные переменные get_local_id(dim), чтобы получить dimлокальный индекс текущего рабочего элемента в измерении. dimЗначение обычно равно 0, что указывает на первое измерение.

  3. Получить глобальную область: используйте встроенные переменные, get_global_size(dim)чтобы получить dimразмер всей области выполнения (количество глобальных рабочих элементов) в измерениях. dimЗначение обычно равно 0, что указывает на первое измерение.

  4. Получение локальной области: используйте встроенную переменную, get_local_size(dim)чтобы получить размер рабочей группы в измерении dim. dimЗначение обычно равно 0, что указывает на первое измерение.

Эти индексы и диапазоны позволяют при необходимости получать доступ к данным в глобальной и локальной памяти. Вот простой пример:

__kernel void myKernel(__global float* input, __global float* output)
{
    
    
    // 获取全局索引和局部索引
    size_t globalId = get_global_id(0);
    size_t localId = get_local_id(0);

    // 获取全局范围和局部范围
    size_t globalSize = get_global_size(0);
    size_t localSize = get_local_size(0);

    // 计算对应全局索引的输入和输出数据索引
    size_t inputIndex = globalId;
    size_t outputIndex = globalId;

    // 计算对应局部索引的输入和输出数据索引
    size_t localInputIndex = localId;
    size_t localOutputIndex = localId;

    // 在此处使用索引访问输入和输出数据,并进行计算
    output[outputIndex] = input[inputIndex] + localInput[localInputIndex];

    // 等待所有工作项完成局部计算
    barrier(CLK_LOCAL_MEM_FENCE);

    // 在此处进行工作组级别的计算或协作

}

В этом примере мы myKernelполучаем глобальный индекс и локальный индекс в функции ядра. Используя эти индексы, мы можем вычислить индексы входных и выходных массивов, к которым осуществляется доступ, и выполнить соответствующие вычислительные операции внутри функции ядра. Вы также можете использовать локальные индексы для вычислений или совместной работы на уровне рабочей группы (например, использование локальной памяти для совместного использования данных). Последняя barrierфункция используется для ожидания завершения локальных вычислений всеми рабочими элементами, чтобы гарантировать синхронное выполнение всех рабочих элементов.

определение контекста opencl

Устройство: коллекция устройств OpenCL, используемых хостом.
Ядро: функции OpenCL, работающие на устройствах OpenCL.
Программный объект: Исходный код программы и исполняемые файлы, реализующие ядро.
объект памяти: групповой объект в памяти, видимый устройству OpenCL, который содержит
значения, которые могут обрабатываться экземплярами ядра.

Строковые программные объекты

Контекст также включает в себя один или несколько программных объектов, содержащих код ядра. Выбор имени программного
объекта может сбить с толку. Лучше всего думать об этом как о динамической библиотеке, из которой можно извлечь функции, используемые ядром
. Программные объекты создаются хост-программой во время выполнения.
Это может показаться странным программистам, не занимающимся графикой . Рассмотрим проблемы, с которыми сталкиваются программисты OpenCL. Он пишет приложение OpenCL и передает его
конечным пользователям, но эти пользователи могут решить запустить приложение в другом месте. Программист не имеет абсолютно никакого
контроля над тем, где конечный пользователь запускает приложение (будь то ЦП, ЦП или другой чип). Все , что знает программист OpenCL,
— это то, что целевая платформа соответствует спецификации OpenCL.
Решением этой проблемы является сборка программного объекта из исходного кода во время выполнения. Хост-программа определяет
устройство в контексте. Только тогда можно узнать, как скомпилировать исходный код программы для создания кода ядра.
OpenCL достаточно гибок по форме относительно самого исходного кода . Во многих случаях это будет обычная строка, которая может быть статически определена в главной программе,
загружена из файла во время выполнения или динамически сгенерирована в главной программе.

Одно и то же устройство, несколько очередей команд

在OpenCL中,您可以在一个设备上创建多个命令队列。这样做主要有以下几个优点:

1. 并发执行:通过使用多个命令队列,您可以并行地执行多个内核或命令,从而提高程序的性能和效率。

2. 独立管理:每个命令队列都有自己的内核执行顺序和状态,因此您可以更灵活地管理和调度不同的内核和任务。

3. 异步操作:使用多个命令队列可以实现异步操作。您可以将多个命令添加到不同的队列中,并在需要时进行同步。

然而,创建多个命令队列也可能有一些注意事项:

1. 设备限制:每个设备的命令队列数量是有限的,具体取决于硬件和驱动程序,超出限制可能导致错误。可以使用`clGetDeviceInfo`函数查询设备支持的最大命令队列数。

2. 内存和资源管理:每个命令队列都有独立的内存和资源管理,因此需要确保系统中的总资源使用不超过设备的限制。

3. 同步和数据共享:不同命令队列之间的同步和数据共享可能需要额外的机制,如事件或内存对象的共享。

为了示范在同一个设备上创建多个命令队列,下面是一个简单的代码片段:

```cpp
cl_device_id device;
cl_context context;
cl_command_queue queue1, queue2;
  
// 创建设备和上下文
clGetDeviceIDs(NULL, CL_DEVICE_TYPE_GPU, 1, &device, NULL);
context = clCreateContext(NULL, 1, &device, NULL, NULL, NULL);

// 创建命令队列
queue1 = clCreateCommandQueue(context, device, 0, NULL);
queue2 = clCreateCommandQueue(context, device, 0, NULL);

// 在命令队列1中提交命令
clEnqueueNDRangeKernel(queue1, kernel1, ...);

// 在命令队列2中提交命令
clEnqueueNDRangeKernel(queue2, kernel2, ...);

В этом примере мы clCreateCommandQueueсоздаем две очереди команд, дважды вызывая функцию: queue1и queue2. Затем мы можем отправлять разные ядра или задачи в эти две очереди команд соответственно.

Следует отметить, что порядок выполнения между различными очередями команд не определен, если вы не используете дополнительные механизмы синхронизации, такие как события или барьеры, для обеспечения их порядка и синхронизации. Кроме того, использование нескольких очередей команд может потребовать более сложного управления задачами и данными для обеспечения надлежащей синхронизации и координации.



```cpp
queue:命令队列对象,用于提交命令。

num_events_in_wait_list:等待事件列表中事件的数量。通常为0,表示没有等待的事件。

event_wait_list:指向等待事件列表的指针,用于指定在执行标记之前需要等待的事件。通常为NULL,表示没有等待的事件。

event:返回的事件对象,用于标识标记命令的事件。在事件完成之前,可以使用该事件来等待或查询内核的执行状态。

Пример одновременного выполнения нескольких функций ядра

#include <CL/cl.h>
#include <iostream>

#define NUM_ELEMENTS 1000
// 在这段代码中,R"(...)" 是一种称为原始字符串字面量(raw string literals)的C++11特性。这种特性允许在字符串中包含特殊字符而无需进行转义。
// 在您给出的代码中,R"( 和 ")" 包围的部分是一个原始字符串字面量,其中包含了一个内核函数的定义。
// 使用原始字符串字面量的好处是,您可以在字符串中直接使用特殊字符,例如反斜杠和双引号,而无需对它们进行转义。这在编写包含许多转义字符的长字符串时特别方便。
const char* kernelSource[] = {
    
    
    R"(
    __kernel void copy(__global int* input, __global int* output) {
        int gid = get_global_id(0);
        output[gid] = input[gid];
    }
    )",

    R"(
    __kernel void doublevalue(__global int* output) {
        int gid = get_global_id(0);
        output[gid] = output[gid]+1;
    }
    )"
};

int main() {
    
    
    cl_platform_id platform;
    cl_device_id device;
    cl_context context;
    cl_command_queue queue;
    cl_program program;
    cl_kernel kernel;
    cl_kernel kernel2;
    cl_mem inputBuffer, outputBuffer;
    cl_event syncEvent;
    int input[NUM_ELEMENTS];
    int output[NUM_ELEMENTS];

    // 初始化输入数据
    for (int i = 0; i < NUM_ELEMENTS; i++) {
    
    
        input[i] = i;
    }

    // 创建平台、设备和上下文
    clGetPlatformIDs(1, &platform, NULL);
    clGetDeviceIDs(platform, CL_DEVICE_TYPE_CPU, 1, &device, NULL);
    context = clCreateContext(NULL, 1, &device, NULL, NULL, NULL);
    queue = clCreateCommandQueue(context, device, 0, NULL);

    // 创建内存对象
    inputBuffer = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(int) * NUM_ELEMENTS, input, NULL);
    outputBuffer = clCreateBuffer(context,  CL_MEM_READ_WRITE, sizeof(int) * NUM_ELEMENTS, NULL, NULL);
    cl_int err;
    // 创建内核程序并构建
    program = clCreateProgramWithSource(context, 2, kernelSource, NULL, &err);
    if (program == NULL || err != CL_SUCCESS) {
    
    
        std::cout << "Failed to create program object." << std::endl;
    }
    //1即num_devices:构建程序的设备数量。
    clBuildProgram(program, 1, &device, NULL, NULL, NULL);
    // 创建内核,第2个参数需要对应核函数名
    kernel = clCreateKernel(program, "copy", NULL);
    kernel2 = clCreateKernel(program, "doublevalue", NULL);

    // 设置内核参数
    clSetKernelArg(kernel, 0, sizeof(cl_mem), &inputBuffer);
    clSetKernelArg(kernel, 1, sizeof(cl_mem), &outputBuffer);
    clSetKernelArg(kernel2, 0, sizeof(cl_mem), &outputBuffer);
    // 创建同步事件
    // syncEvent = clCreateUserEvent(context, NULL);
    size_t globalSize[1]={
    
    NUM_ELEMENTS};
    //注意:在同一个命令队列中,对于相同的命令队列上的内核函数,它们通常是按顺序执行的。
    //这意味着在命令队列中提交的内核函数将按照它们被插入的顺序进行执行。
    //执行第一个内核
    clEnqueueNDRangeKernel(queue, kernel, 1, NULL, globalSize, NULL, 0, NULL, NULL);

    // 执行第二个内核(在第一个内核完成后执行)第二个1为在内核程序执行前等待的数量
    clEnqueueNDRangeKernel(queue, kernel2, 1, NULL, globalSize, NULL, 0, NULL, NULL);

    // 读取结果
    clEnqueueReadBuffer(queue, outputBuffer, CL_TRUE, 0, sizeof(int) * NUM_ELEMENTS, output, 0, NULL, NULL);

    // 打印结果
    for (int i = 0; i < NUM_ELEMENTS; i++) {
    
    
        std::cout << "Output[" << i << "] = " << output[i] << std::endl;
    }

    // 释放资源
    clReleaseMemObject(inputBuffer);
    clReleaseMemObject(outputBuffer);
    clReleaseKernel(kernel);
    clReleaseKernel(kernel2);
    clReleaseProgram(program);
    clReleaseCommandQueue(queue);
    clReleaseContext(context);
    return 0;
}

Supongo que te gusta

Origin blog.csdn.net/hh1357102/article/details/132564011
Recomendado
Clasificación