OpenCL Как восстановить буферы при использовании нескольких устройств?
Я изучаю себя openCL на Java, используя библиотеки jogamp jocl. Один из моих тестов - создание карты Мандельброта. У меня есть четыре теста: простой последовательный, параллельный с использованием интерфейса Java-исполнителя, openCL для одного устройства и openCL для нескольких устройств. Первые три в порядке, последний нет. Когда я сравниваю (правильный) вывод нескольких устройств с неправильным выводом решения для нескольких устройств, я замечаю, что цвета примерно одинаковы, но вывод последнего искажен. Я думаю, что понимаю, в чем проблема, но не могу ее решить.
Беда (imho) в том, что openCL использует векторные буферы и что мне нужно преобразовать вывод в матрицу. Я думаю, что этот перевод неверен. Я парализую код, разделив карту Мандельброта на прямоугольники, в которых ширина (xSize) делится на количество задач, а высота (ySize) сохраняется. Я думаю, что я могу правильно передать эту информацию в ядро, но переводить ее обратно некорректно.
CLMultiContext mc = CLMultiContext.create (deviceList);
try
{
CLSimpleContextFactory factory = CLQueueContextFactory.createSimple (programSource);
CLCommandQueuePool<CLSimpleQueueContext> pool = CLCommandQueuePool.create (factory, mc);
IntBuffer dataC = Buffers.newDirectIntBuffer (xSize * ySize);
IntBuffer subBufferC = null;
int tasksPerQueue = 16;
int taskCount = pool.getSize () * tasksPerQueue;
int sliceWidth = xSize / taskCount;
int sliceSize = sliceWidth * ySize;
int bufferSize = sliceSize * taskCount;
double sliceX = (pXMax - pXMin) / (double) taskCount;
String kernelName = "Mandelbrot";
out.println ("sliceSize: " + sliceSize);
out.println ("sliceWidth: " + sliceWidth);
out.println ("sS*h:" + sliceWidth * ySize);
List<CLTestTask> tasks = new ArrayList<CLTestTask> (taskCount);
for (int i = 0; i < taskCount; i++)
{
subBufferC = Buffers.slice (dataC, i * sliceSize, sliceSize);
tasks.add (new CLTestTask (kernelName, i, sliceWidth, xSize, ySize, maxIterations,
pXMin + i * sliceX, pYMin, xStep, yStep, subBufferC));
} // for
pool.invokeAll (tasks);
// submit blocking immediately
for (CLTestTask task: tasks) pool.submit (task).get ();
// Ready read the buffer into the frequencies matrix
// according to me this is the part that goes wrong
int w = taskCount * sliceWidth;
for (int tc = 0; tc < taskCount; tc++)
{
int offset = tc * sliceWidth;
for (int y = 0; y < ySize; y++)
{
for (int x = offset; x < offset + sliceWidth; x++)
{
frequencies [y][x] = dataC.get (y * w + x);
} // for
} // for
} // for
pool.release();
Последний цикл является виновником, что означает (я думаю) несоответствие между кодировкой ядра и трансляцией хоста. Ядро:
kernel void Mandelbrot
(
const int width,
const int height,
const int maxIterations,
const double x0,
const double y0,
const double stepX,
const double stepY,
global int *output
)
{
unsigned ix = get_global_id (0);
unsigned iy = get_global_id (1);
if (ix >= width) return;
if (iy >= height) return;
double r = x0 + ix * stepX;
double i = y0 + iy * stepY;
double x = 0;
double y = 0;
double magnitudeSquared = 0;
int iteration = 0;
while (magnitudeSquared < 4 && iteration < maxIterations)
{
double x2 = x*x;
double y2 = y*y;
y = 2 * x * y + i;
x = x2 - y2 + r;
magnitudeSquared = x2+y2;
iteration++;
}
output [iy * width + ix] = iteration;
}
Последний оператор кодирует информацию в вектор. Это ядро также используется версией для одного устройства. Разница лишь в том, что в версии для нескольких устройств я изменил ширину и х0. Как вы можете видеть в коде Java, я передаю xSize / number_of_tasks
как ширина и pXMin + i * sliceX
как x0 (вместо pXMin).
Я работаю над этим уже несколько дней и удалил довольно много ошибок, но я больше не могу видеть, что я делаю сейчас неправильно. Помощь очень ценится.
Редактировать 1
@Huseyin попросил изображение. Первый скриншот, рассчитанный на одном устройстве openCL.
Второй снимок экрана - версия для нескольких устройств, рассчитанная с точно такими же параметрами.
Редактировать 2
Возник вопрос о том, как я ставлю в очередь буферы. Как вы можете видеть в коде выше, у меня есть list<CLTestTask>
к которому я добавляю задачи и в котором буфер ставится в очередь. CLTestTask - это внутренний класс, код которого вы можете найти ниже.
последний класс CLTestTask реализует CLTask { CLBuffer clBufferC = null; Buffer bufferSliceC; String kernelName; int index; int sliceWidth; int width; высота int; int maxIterations; двойной pXMin; двойной pYMin; двойной x_step; double y_step;
public CLTestTask
(
String kernelName,
int index,
int sliceWidth,
int width,
int height,
int maxIterations,
double pXMin,
double pYMin,
double x_step,
double y_step,
Buffer bufferSliceC
)
{
this.index = index;
this.sliceWidth = sliceWidth;
this.width = width;
this.height = height;
this.maxIterations = maxIterations;
this.pXMin = pXMin;
this.pYMin = pYMin;
this.x_step = x_step;
this.y_step = y_step;
this.kernelName = kernelName;
this.bufferSliceC = bufferSliceC;
} /*** CLTestTask ***/
public Buffer execute (final CLSimpleQueueContext qc)
{
final CLCommandQueue queue = qc.getQueue ();
final CLContext context = qc.getCLContext ();
final CLKernel kernel = qc.getKernel (kernelName);
clBufferC = context.createBuffer (bufferSliceC);
out.println (pXMin + " " + sliceWidth);
kernel
.putArg (sliceWidth)
.putArg (height)
.putArg (maxIterations)
.putArg (pXMin) // + index * x_step)
.putArg (pYMin)
.putArg (x_step)
.putArg (y_step)
.putArg (clBufferC)
.rewind ();
queue
.put2DRangeKernel (kernel, 0, 0, sliceWidth, height, 0, 0)
.putReadBuffer (clBufferC, true);
return clBufferC.getBuffer ();
} /*** execute ***/
} /*** Inner Class: CLTestTask ***/
1 ответ
Вы создаете подбуферы с
subBufferC = Buffers.slice (dataC, i * sliceSize, sliceSize);
и они имеют данные в памяти как:
0 1 3 10 11 12 19 20 21 28 29 30
4 5 6 13 14 15 22 23 24 31 32 33
7 8 9 16 17 18 25 26 27 34 35 36
с помощью команд копирования прямоугольника opencl? Если это так, то вы получаете доступ к ним за пределами
output [iy * width + ix] = iteration;
так как width
больше чем sliceWidth
и записывает границы в ядре.
Если вы не делаете прямоугольные копии или подпуферы и просто берете смещение от исходного буфера, то у него есть структура памяти, подобная
0 1 3 4 5 6 7 8 9 | 10 11 12
13 14 15 16 17 18|19 20 21 22 23 24
25 26 27|28 29 30 31 32 33 34 35 36
поэтому к массивам обращаются / интерпретируют как искаженные или неправильно вычисленные.
Вы даете смещение в качестве параметра ядра. Но вы также можете сделать это из параметров очереди ядра. Таким образом, i и j будут начинаться с их истинных значений (вместо нуля), и вам не нужно будет добавлять x0 или y0 к ним в ядре для всех потоков.
Я уже писал API для нескольких устройств. Он использует несколько буферов, по одному для каждого устройства, и все они равны по размеру основному буферу. И они просто копируют необходимые части (свою собственную территорию) в / из основного буфера (хост-буфера), чтобы вычисления ядра оставались абсолютно одинаковыми для всех устройств с использованием надлежащих смещений глобального диапазона. Недостатком является то, что основной буфер буквально дублируется на всех устройствах. Если у вас есть 4 gpus и 1 ГБ данных, вам нужно всего 4 ГБ буферной области. Но таким образом, компоненты ядра намного легче читать, независимо от того, сколько устройств используется.
Если вы выделяете только 1/N размер буферов на устройство (из N устройств), то вам нужно скопировать с 0-го адреса суббуфера в i*sliceHeight
основного буфера, где i - индекс устройства, учитывая, что массивы плоские, поэтому для каждого устройства нужна команда копирования прямоугольного буфера opencl api. Я подозреваю, что вы также используете плоские массивы и используете прямоугольные копии и переполнение за пределами ядра. Тогда я предлагаю:
- удалите все связанные с устройством смещения и параметры из ядра
- добавить необходимые смещения в параметры очереди ядра, а не аргументы
- дублировать основной буфер на каждом устройстве, если вы еще этого не сделали
- копировать только необходимые части, относящиеся к устройствам (непрерывное, если плоское деление массива, прямоугольные копии для 2D интерпретации / деления массива)
если целые данные не помещаются на устройстве, вы можете попробовать отобразить / удалить, чтобы они не выделялись в фоновом режиме. На своей странице это говорит:
Несколько командных очередей могут отображать область или перекрывающиеся области объекта памяти для чтения (т.е. map_flags = CL_MAP_READ). Содержимое областей объекта памяти, отображенного для чтения, также может быть прочитано ядрами, выполняющимися на устройстве (ах). Поведение записей, выполняемых ядром на устройстве в отображенную область объекта памяти, не определено. Отображение (и отображение) перекрывающихся областей буфера или объекта памяти изображения для записи не определено.
и в нем не говорится, что "неперекрывающиеся отображения для чтения / записи не определены", поэтому у вас должно быть все в порядке, чтобы отображения были на каждом устройстве для одновременного чтения / записи в целевом буфере. Но при использовании с флагом USE_HOST_PTR (для максимальной производительности потоковой передачи) для каждого подпуфера может потребоваться выровненный указатель для начала, что может затруднить разбиение области на соответствующие фрагменты. Я использую один и тот же массив данных для всех устройств, так что это не проблема для разделения работы, так как я могу отобразить unmap любой адрес в выровненном буфере.
Вот результат для двух устройств с 1-D делением (верхняя часть от процессора, нижняя часть от процессора):
и это внутри ядра:
unsigned ix = get_global_id (0)%w2;
unsigned iy = get_global_id (0)/w2;
if (ix >= w2) return;
if (iy >= h2) return;
double r = ix * 0.001;
double i = iy * 0.001;
double x = 0;
double y = 0;
double magnitudeSquared = 0;
int iteration = 0;
while (magnitudeSquared < 4 && iteration < 255)
{
double x2 = x*x;
double y2 = y*y;
y = 2 * x * y + i;
x = x2 - y2 + r;
magnitudeSquared = x2+y2;
iteration++;
}
b[(iy * w2 + ix)] =(uchar4)(iteration/5.0,iteration/5.0,iteration/5.0,244);
Потребовалось 17 мс с FX8150(7 ядер на 3,7 ГГц) + R7_240 на 700 МГц для изображения размером 512x512 (8 бит на канал + альфа)
Кроме того, наличие суббуферов, равных размеру буфера хоста, позволяет быстрее (без перераспределения) использовать динамические диапазоны, а не статические (в случае гетерогенной настройки, динамических турбо-частот и отклонений / дросселей), чтобы помочь динамической балансировке нагрузки. В сочетании с мощью "одинаковые коды и одинаковые параметры" это не влечет за собой снижения производительности. Например, c[i]=a[i]+b[i]
потребуется c[i+i0]=a[i+i0]+b[i+i0]
работать на нескольких устройствах, если все ядра начинаются с нуля и добавили бы больше циклов (кроме узкого места в памяти и читабельности и странности распределения c=a+b).