Хорошо, я постараюсь объяснить это как можно лучше, но вы задали целую кучу вопросов в одном посте, и, похоже, вам не хватает базовых абстракций OpenCL.
Хост : именно хост решает, что произойдет с OpenCL. Это процессор, который запускает вашу программу.
Вычислительное устройство : это оборудование, на котором будет выполняться ваш код. Графическая карта (GPU) - это одно устройство, как и многоядерный процессор. Если на вашем компьютере установлено два графических процессора, ваша программа может быть запущена на обоих устройствах одновременно.
Вычислительный блок : Внутри устройства все ядра (ядра CUDA для Nvidia, процессоры Stream для AMD) разделены на группы, которые совместно используют общую локальную память. Каждый вычислительный блок концептуально можно рассматривать как небольшой SIMD-процессор. Размеры групп варьируются от одного устройства к другому, но обычно это 32 или 64. (Для моего GTX 970 у меня 1664 ядра CUDA в 13 вычислительных блоках, то есть 128). Я не думаю, что есть прямой способ сделать запрос с помощью clGetDeviceInfo
, но вы легко можете понять это для данной видеокарты.
Элемент обработки : Так мы называем одно ядро в GPU. У них нет никакой памяти, только регистры. Обратите внимание, что в любой момент времени каждый обрабатывающий элемент одного и того же вычислительного устройства будет синхронизировать одну и ту же программу. Если в вашем коде есть куча логических (if/else
) операторов и некоторые обрабатывающие элементы занимают ветку, отличную от других, все остальные будут ждать, ничего не делая.
Программа : это более или менее понятно даже мне. Это ваша программа, которая загружается в память и должна быть собрана / скомпилирована. Программа может содержать несколько функций (ядер), которые вызываются по отдельности.
Ядро : проще говоря, это функция, которую вы будете запускать на своем устройстве. Экземпляр ядра будет работать на элементе обработки. Существует множество экземпляров вашего ядра, работающих одновременно. Внутри ядра можно получить некоторую информацию об элементе обработки, на котором он выполняется. Это делается с помощью некоторых основных функций, тесно связанных с параметрами clEnqueueNDRangeKernel (см. Ниже).
Память:
С точки зрения памяти, каждое вычислительное устройство (GPU) имеет глобальную память, в которую вы можете записывать данные с хоста. Затем каждый вычислительный блок будет иметь ограниченный объем локальной памяти (CL_DEVICE_LOCAL_MEM_SIZE), который совместно используется элементами обработки этого вычислительного блока. Существует ряд ограничений в отношении размера буферов, которые вы можете выделить, но обычно это не проблема. Вы можете запросить различные параметры CL_DEVICE_x, чтобы получить эти числа. Глобальная память имеет «постоянную» часть, но я не буду ее обсуждать, поскольку она не принесет ничего в обсуждение.
Если вы хотите выполнять вычисления на GPU, вам нужно ядро и некоторые буферы в памяти GPU. Хост (ЦП) должен передавать память в буфер в глобальной памяти. Также следует установить аргументы, требуемые ядром. Затем он должен сообщить GPU, чтобы оно вызывало ядро с помощью clEnqueueNDRangeKernel. Эта функция имеет несколько параметров ...
globalWorkSize : количество раз, которое ваше ядро должно запускать для решения вашей проблемы, для каждого измерения. Количество измерений является произвольным, согласно стандарту вычислительное устройство должно поддерживать как минимум 3 измерения, но некоторые графические процессоры могут поддерживать больше. На самом деле это не имеет значения, так как любая проблема ND может быть разбита на несколько проблем 1D.
localWorkSize : это размер работы, выполняемой вычислительной единицей для каждого измерения. Обычно вы хотите использовать значение, которое соответствует количеству элементов обработки в ваших вычислительных единицах (обычно 32 или 64, см. Выше). Обратите внимание, что localWorkSize должен делить globalWorkSize равномерно. 0 == (globalWorkSize % localWorkSize)
.
Давайте приведем это в пример.Скажем, у меня есть одномерный массив из 1024 чисел, и я просто хочу вывести каждое значение в этом массиве.GlobalWorkSize равен 1024, потому что я хочу, чтобы каждое число обрабатывалось независимо, и я установил бы для localWorkSize наибольшее количество обрабатывающих элементов в моем вычислительном блоке, которое делит равномерно 1024 (я буду использовать 128 для моего GTX970).У меня проблема с 1-м измерением, поэтому я напишу 1 для этого параметра.
Имейте в виду, что если вы используете меньшее (или большее) число, чем количество элементов обработки в ваших вычислительных единицах,другие будут просто записывать такты, ничего не делая. Я мог бы сказать, что я хочу, чтобы localWorkSize равнялся 2, но тогда каждая вычислительная единица потратила бы 126/128 обрабатывающих элементов, и это не очень эффективно.
При установкеglobalWorkSize = 1024
и localWorkSize = 128
, я только что сказал моему GPU запускать ядро 1024 раза на (1024/128 = 8) вычислительных блоках.У меня будет 1024 обрабатывающих элемента (ядра CUDA), каждый из которых выполняет операцию с 1 элементом моего буфера.
Теперь, как обрабатывающий элемент узнает, какое значение он должен вычислить в моем буфере?Вот где функции рабочего элемента входят в игру.
Их несколько, но для этого примера я буду заботиться только о get_global_id(uint nDimensions)
.Он возвращает глобальный идентификатор для данного измерения на основе globalWorkSize.В нашем случае наша проблема - 1d, поэтому get_global_id(0)
вернет индекс между [0, globalWorkSize].Индекс отличается для каждого элемента обработки.
Пример ядра:
__kernel MakeSquared(__global double* values) {
size_t idx = get_global_id(0);
values[idx] = values[idx] * values[idx];
}
РЕДАКТИРОВАТЬ: пример с использованием локальной памяти:
__kernel MakeSquared(__global double* values, __local double* lValues) {
size_t idx = get_global_id(0);
size_t localId = get_local_id(0);
lValues[localId] = values[idx];
// potentially some complex calculations
lValues[localId] = lValues[localId] * lValues[localId];
values[idx] = lValues[localId];
}
Осталось так много всегосказать, но я думаю, что я охватил основы.