OpenCL 如何在使用多个设备时重建缓冲区?
OpenCL How to reconstruct buffers when using multiple devices?
我正在使用 jogamp jocl 库在 Java 中学习 openCL。我的一项测试是制作 Mandelbrot 地图。我有四个测试:简单的串行、使用 Java 执行程序接口的并行、用于单个设备的 openCL 和用于多个设备的 openCL。前三个可以,最后一个不行。当我将多设备的(正确)输出与多设备解决方案的错误输出进行比较时,我注意到颜色大致相同,但最后一个的输出是乱码。我想我明白问题出在哪里,但我无法解决它。
问题在于(恕我直言)openCL 使用向量缓冲区,我必须将输出转换为矩阵。我认为这个翻译是不正确的。我通过将 mandelbrot 图划分为矩形来并行化代码,其中宽度 (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;
}
最后一条语句将信息编码到向量中。这个内核也被单设备版本使用。唯一的区别是在多设备版本中我改变了宽度和 x0。正如您在 Java 代码中看到的,我将 xSize / number_of_tasks
作为宽度传输,将 pXMin + i * sliceX
作为 x0(而不是 pXMin)传输。
我已经为此工作了几天,并且已经删除了很多错误,但我现在再也看不到我做错了什么。非常感谢帮助。
编辑 1
@Huseyin 要了一张图片。由 openCL 单个设备计算的第一个屏幕截图。
第二张截图是多设备版本,使用完全相同的参数计算。
编辑 2
关于我如何对缓冲区进行排队的问题。正如你在上面的代码中看到的那样,我有一个 list<CLTestTask>
,我向其中添加了任务,并且在其中排队了缓冲区。 CLTestTask 是一个内部 class,您可以在下面找到其中的代码。
final class CLTestTask 实现 CLTask
{
CLBuffer clBufferC = null;
缓冲区 bufferSliceC;
字符串内核名称;
整数索引;
int 切片宽度;
整数宽度;
整数高度;
int 最大迭代次数;
双pXMin;
双pYMin;
双 x_step;
双 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 ***/
您正在使用
创建子缓冲区
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
因此数组 accessed/interpreted 存在偏差或计算错误。
您将偏移量作为内核的参数。但是你也可以从内核入队参数中做到这一点。所以 i 和 j 将从它们的真实值(而不是零)开始,你不需要在内核中为所有线程添加 x0 或 y0。
我以前写过一个多设备api。它使用多个缓冲区,每个设备一个,它们的大小都与主缓冲区相同。他们只是复制必要的部分(他们自己的领土)to/from 主缓冲区(主机缓冲区),因此内核计算与所有设备保持完全相同,并使用适当的全局范围偏移量。不利的一面是,主缓冲区在所有设备上都是重复的。如果你有 4 个 gpus 和 1GB 数据,你总共需要 4GB 缓冲区。但这样一来,无论使用多少设备,内核成分都更容易阅读。
如果您只为每个设备分配 1/N 大小的缓冲区(在 N 个设备中),那么您需要从子缓冲区的第 0 个地址复制到主缓冲区的 i*sliceHeight
,其中 i 是设备索引,考虑到数组是扁平的,因此每个设备都需要 opencl api 的矩形缓冲区复制命令。我怀疑您也在使用平面数组并在内核中使用矩形副本和溢出越界。那我建议:
- 从内核中删除任何与设备相关的偏移量和参数
- 将必要的偏移量添加到内核入队参数中,而不是参数
- 在每个设备上复制主缓冲区,如果你还没有这样做的话
- 仅复制与设备相关的必要部分(如果平面数组划分是连续的,则数组的二维 interpretation/division 是矩形复制)
如果设备无法容纳整个数据,您可以尝试 mapping/unmapping 这样就不会在后台分配太多数据。在它的页面上写着:
Multiple command-queues can map a region or overlapping regions of a
memory object for reading (i.e. map_flags = CL_MAP_READ). The contents
of the regions of a memory object mapped for reading can also be read
by kernels executing on a device(s). The behavior of writes by a
kernel executing on a device to a mapped region of a memory object is
undefined. Mapping (and unmapping) overlapped regions of a buffer or
image memory object for writing is undefined.
它没有说,"non-overlapped mappings for read/write are undefined" 所以你应该可以在每个设备上为目标缓冲区上的并发 read/write 映射。但是当与 USE_HOST_PTR 标志一起使用时(为了最大流性能),每个子缓冲区可能需要有一个对齐的指针开始,这可能会使将区域分割成适当的块变得更加困难。我对所有设备使用相同的整个数据数组,因此划分工作不是问题,因为我可以在对齐的缓冲区中映射取消映射任何地址。
这是一维除法的 2 设备结果(上半部分 cpu,下半部分 gpu):
这是内核内部:
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);
对于 512x512 大小的图像(每通道 8 位 + alpha),使用 FX8150(7 个内核,3.7GHz)+ R7_240 在 700 MHz 时花费 17 毫秒
子缓冲区的大小与主机缓冲区的大小相同,可以更快(无需重新分配)使用动态范围而不是静态范围(在异构设置、动态涡轮频率和 hiccups/throttles 的情况下),以帮助动态负载均衡。结合 "same codes same parameters" 的强大功能,它不会导致性能下降。例如,c[i]=a[i]+b[i]
需要 c[i+i0]=a[i+i0]+b[i+i0]
才能在多个设备上工作,如果所有内核都从零开始并且会增加更多的周期(除了内存瓶颈和可读性以及分布 c=a+b 的怪异)。
我正在使用 jogamp jocl 库在 Java 中学习 openCL。我的一项测试是制作 Mandelbrot 地图。我有四个测试:简单的串行、使用 Java 执行程序接口的并行、用于单个设备的 openCL 和用于多个设备的 openCL。前三个可以,最后一个不行。当我将多设备的(正确)输出与多设备解决方案的错误输出进行比较时,我注意到颜色大致相同,但最后一个的输出是乱码。我想我明白问题出在哪里,但我无法解决它。
问题在于(恕我直言)openCL 使用向量缓冲区,我必须将输出转换为矩阵。我认为这个翻译是不正确的。我通过将 mandelbrot 图划分为矩形来并行化代码,其中宽度 (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;
}
最后一条语句将信息编码到向量中。这个内核也被单设备版本使用。唯一的区别是在多设备版本中我改变了宽度和 x0。正如您在 Java 代码中看到的,我将 xSize / number_of_tasks
作为宽度传输,将 pXMin + i * sliceX
作为 x0(而不是 pXMin)传输。
我已经为此工作了几天,并且已经删除了很多错误,但我现在再也看不到我做错了什么。非常感谢帮助。
编辑 1
@Huseyin 要了一张图片。由 openCL 单个设备计算的第一个屏幕截图。
第二张截图是多设备版本,使用完全相同的参数计算。
编辑 2
关于我如何对缓冲区进行排队的问题。正如你在上面的代码中看到的那样,我有一个 list<CLTestTask>
,我向其中添加了任务,并且在其中排队了缓冲区。 CLTestTask 是一个内部 class,您可以在下面找到其中的代码。
final class CLTestTask 实现 CLTask { CLBuffer clBufferC = null; 缓冲区 bufferSliceC; 字符串内核名称; 整数索引; int 切片宽度; 整数宽度; 整数高度; int 最大迭代次数; 双pXMin; 双pYMin; 双 x_step; 双 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 ***/
您正在使用
创建子缓冲区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
因此数组 accessed/interpreted 存在偏差或计算错误。
您将偏移量作为内核的参数。但是你也可以从内核入队参数中做到这一点。所以 i 和 j 将从它们的真实值(而不是零)开始,你不需要在内核中为所有线程添加 x0 或 y0。
我以前写过一个多设备api。它使用多个缓冲区,每个设备一个,它们的大小都与主缓冲区相同。他们只是复制必要的部分(他们自己的领土)to/from 主缓冲区(主机缓冲区),因此内核计算与所有设备保持完全相同,并使用适当的全局范围偏移量。不利的一面是,主缓冲区在所有设备上都是重复的。如果你有 4 个 gpus 和 1GB 数据,你总共需要 4GB 缓冲区。但这样一来,无论使用多少设备,内核成分都更容易阅读。
如果您只为每个设备分配 1/N 大小的缓冲区(在 N 个设备中),那么您需要从子缓冲区的第 0 个地址复制到主缓冲区的 i*sliceHeight
,其中 i 是设备索引,考虑到数组是扁平的,因此每个设备都需要 opencl api 的矩形缓冲区复制命令。我怀疑您也在使用平面数组并在内核中使用矩形副本和溢出越界。那我建议:
- 从内核中删除任何与设备相关的偏移量和参数
- 将必要的偏移量添加到内核入队参数中,而不是参数
- 在每个设备上复制主缓冲区,如果你还没有这样做的话
- 仅复制与设备相关的必要部分(如果平面数组划分是连续的,则数组的二维 interpretation/division 是矩形复制)
如果设备无法容纳整个数据,您可以尝试 mapping/unmapping 这样就不会在后台分配太多数据。在它的页面上写着:
Multiple command-queues can map a region or overlapping regions of a memory object for reading (i.e. map_flags = CL_MAP_READ). The contents of the regions of a memory object mapped for reading can also be read by kernels executing on a device(s). The behavior of writes by a kernel executing on a device to a mapped region of a memory object is undefined. Mapping (and unmapping) overlapped regions of a buffer or image memory object for writing is undefined.
它没有说,"non-overlapped mappings for read/write are undefined" 所以你应该可以在每个设备上为目标缓冲区上的并发 read/write 映射。但是当与 USE_HOST_PTR 标志一起使用时(为了最大流性能),每个子缓冲区可能需要有一个对齐的指针开始,这可能会使将区域分割成适当的块变得更加困难。我对所有设备使用相同的整个数据数组,因此划分工作不是问题,因为我可以在对齐的缓冲区中映射取消映射任何地址。
这是一维除法的 2 设备结果(上半部分 cpu,下半部分 gpu):
这是内核内部:
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);
对于 512x512 大小的图像(每通道 8 位 + alpha),使用 FX8150(7 个内核,3.7GHz)+ R7_240 在 700 MHz 时花费 17 毫秒
子缓冲区的大小与主机缓冲区的大小相同,可以更快(无需重新分配)使用动态范围而不是静态范围(在异构设置、动态涡轮频率和 hiccups/throttles 的情况下),以帮助动态负载均衡。结合 "same codes same parameters" 的强大功能,它不会导致性能下降。例如,c[i]=a[i]+b[i]
需要 c[i+i0]=a[i+i0]+b[i+i0]
才能在多个设备上工作,如果所有内核都从零开始并且会增加更多的周期(除了内存瓶颈和可读性以及分布 c=a+b 的怪异)。