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).

Другие вопросы по тегам