Каковы размеры диапазона openCL? - PullRequest
0 голосов
/ 30 апреля 2019

Я думал, что ускоритель вычислений (GPU) - это некий набор SP - « S tream P rocessors», каждый из которых состоит из некоторого фиксированного числа ядер ALU, работает в SIMD манере. Но в отличие от процессорных потоков, SP запускаются вместе, с некоторым шагом. Это также называется объединением.

Так, например, у меня есть массивы A, B и C. Я бы знал и управлял их внутренней структурой (будь то массивы 1D или 5D) - это не представляет интереса для GPU. Я просто говорю это как - «Возьмите эту пару только для чтения памяти A и B. Возьмите ту, которая только для записи памяти C. Выполните некоторую последовательность инструкций N раз.»

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

Итак, лицевая сторона монеты такова, что каждый ДРАМ является ПЛОСКОЙ. Так что все в ПК является одномерным по своей природе. Я не понимаю, что такое 2D, 3D диапазоны и для чего они используются. Разве мы не можем просто использовать 1D везде?

С другой стороны, давайте предположим, что это сделано, потому что подход openCL утверждает, что он очень гибок, чтобы даже заставить нас предоставить ему структуру внутренних массивов. Теперь у меня есть 42-мерные данные! Почему он не поддерживается, но поддерживается только 3 измерения?

Итак, что такое локальные, глобальные группы, измерения ndranges и как их вычислять?

Не могли бы вы привести пример, в котором многомерные диапазоны имеют решающее значение или, по крайней мере, выгодны для использования? Как разделить на локальный, кеш и глобальный размеры?

Вот список параметров, которые я не понимаю, и которые полностью запутаны в этом:

CL_DEVICE_GLOBAL_MEM_CACHE_SIZE
CL_DEVICE_GLOBAL_MEM_CACHELINE_SIZE
CL_DEVICE_MAX_CONSTANT_BUFFER_SIZE
CL_DEVICE_LOCAL_MEM_SIZE
CL_DEVICE_MAX_WORK_ITEM_SIZES
CL_DEVICE_IMAGE2D_MAX_HEIGHT
CL_DEVICE_IMAGE3D_MAX_HEIGHT
CL_DEVICE_MAX_SAMPLERS
CL_DEVICE_MAX_COMPUTE_UNITS

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

Ответы [ 2 ]

2 голосов
/ 30 апреля 2019

Хорошо, я постараюсь объяснить это как можно лучше, но вы задали целую кучу вопросов в одном посте, и, похоже, вам не хватает базовых абстракций 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];
}

Осталось так много всегосказать, но я думаю, что я охватил основы.

0 голосов
/ 02 мая 2019

Но в отличие от процессорных потоков, SP запускаются вместе, с некоторым шагом. Это также называется объединением.

Здесь вы смешиваете две разные концепции: доступ к памяти (/ объединение) и выполнение программы. Каждый PE в CU выполняет одну и ту же инструкцию на шаге (по крайней мере, на большинстве графических процессоров, есть некоторые исключения), но любой шаг или объединение остается за программистом. Например, я могу написать ядро, которое работает с глобальным рабочим размером 1000, но все 1000 рабочих элементов будут иметь доступ только к 10 байтам памяти. Или просто 1 байт. Или 10 мегабайт в случайном порядке. Доступ к памяти / объединение не зависит от диапазонов выполнения программы (глобальные / локальные размеры работы). IOW локальные / глобальные диапазоны указывают, сколько экземпляров вашего ядра будет запущено; но то, как каждый экземпляр обращается к памяти, не имеет к этому никакого отношения.

DRAM - это квартира. Так что все в ПК является одномерным по своей природе. Я не понимаю, что такое 2D, 3D диапазоны и для чего они используются. Разве мы не можем просто использовать 1D везде?

Опять же, диапазоны не имеют ничего общего с памятью. Относительно того, почему существуют 2D / 3D диапазоны:

Допустим, у вас есть 2D-изображение 800x600, и вы хотите запустить фильтр sobel. Если у вас есть только 1D диапазон, вы можете запустить свое ядро ​​на каждом пикселе с глобальным размером 1D 480000. Но для фильтра sobel требуются пиксели из предыдущей и следующей строк изображения. Таким образом, вам придется пересчитать x и y текущего пикселя из значения 1D - и это требует деления и по модулю. Оба медленные, и вы должны сделать это для каждого пикселя. Смысл наличия 2D / 3D диапазонов заключается в том, что get_global_id и друзья работают с аппаратным ускорением. Обычно некоторые аппаратные средства в графическом процессоре (планировщик, CU или PE) отслеживают x, y, z текущего выполняемого рабочего элемента в некоторых специальных регистрах, а get_global_id преобразуется в одну инструкцию, которая читает регистры.

42-мерные данные! Почему он не поддерживается, но поддерживается только 3 измерения?

Поскольку архитекторы графических процессоров не видели смысла в ускорении get_global_id & friends для более чем трех измерений.

...