如何减少OpenCL / Cloo(C#)缓冲区创建的开销?

时间:2017-02-23 23:13:01

标签: c# opencl cloo

我通过C#Cloo界面使用OpenCL,并且在尝试让它在我们的产品中运行良好时遇到了一些非常令人沮丧的问题。

我们的产品不是太多,而是一款计算机视觉产品,每秒三十秒,从我们的相机中获得512x424像素值网格。我们希望对这些像素进行计算,以生成相对于场景中某些对象的点云。

我在尝试计算这些像素时所做的是,当我们得到一个新帧时,以下(每一帧):

1)创建一个CommandQueue, 2)创建一个只读输入像素值的缓冲区, 3)创建一个零拷贝缓冲区,仅对输出点值进行写入。 4)传入矩阵以在GPU上进行计算, 5)执行内核并等待响应。

每帧工作的一个例子是:

        // the command queue is the, well, queue of commands sent to the "device" (GPU)
        ComputeCommandQueue commandQueue = new ComputeCommandQueue(
            _context, // the compute context
            _context.Devices[0], // first device matching the context specifications
            ComputeCommandQueueFlags.None); // no special flags

        Point3D[] realWorldPoints = points.Get(Perspective.RealWorld).Points;
        ComputeBuffer<Point3D> realPointsBuffer = new ComputeBuffer<Point3D>(_context,
            ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.UseHostPointer,
            realWorldPoints);
        _kernel.SetMemoryArgument(0, realPointsBuffer);

        Point3D[] toPopulate = new Point3D[realWorldPoints.Length];
        PointSet pointSet = points.Get(perspective);

        ComputeBuffer<Point3D> resultBuffer = new ComputeBuffer<Point3D>(_context,
            ComputeMemoryFlags.UseHostPointer,
            toPopulate);
        _kernel.SetMemoryArgument(1, resultBuffer);
            float[] M = new float[3 * 3];
            ReferenceFrame referenceFrame =
                perspectives.ReferenceFrames[(int)Perspective.Floor];
            AffineTransformation transform = referenceFrame.ToReferenceFrame;
            M[0] = transform.M00;
            M[1] = transform.M01;
            M[2] = transform.M02;
            M[3] = transform.M10;
            M[4] = transform.M11;
            M[5] = transform.M12;
            M[6] = transform.M20;
            M[7] = transform.M21;
            M[8] = transform.M22;

            ComputeBuffer<float> Mbuffer = new ComputeBuffer<float>(_context,
                ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.UseHostPointer,
                M);
            _kernel.SetMemoryArgument(2, Mbuffer);

            float[] b = new float[3];
            b[0] = transform.b0;
            b[1] = transform.b1;
            b[2] = transform.b2;

            ComputeBuffer<float> Bbuffer = new ComputeBuffer<float>(_context,
                ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.UseHostPointer,
                b);
            _kernel.SetMemoryArgument(3, Bbuffer);

            _kernel.SetValueArgument<int>(4, (int)Perspective.Floor);

            //sw.Start();

            commandQueue.Execute(_kernel,
                new long[] { 0 }, new long[] { toPopulate.Length }, null, null);
            IntPtr retPtr = commandQueue.Map(
                resultBuffer,
                true,
                ComputeMemoryMappingFlags.Read,
                0,
                toPopulate.Length, null);

            commandQueue.Unmap(resultBuffer, ref retPtr, null);

分析时,WAAAY的时间太长,90%的时间都是在创建所有ComputeBuffer对象等时弥补的.GPU上的实际计算时间很快。

我的问题是,我该如何解决这个问题?进来的像素数组对于每一帧都是不同的,所以我必须为此创建一个新的ComputeBuffer。当我们更新场景时,我们的矩阵也会定期更改(同样,我不能详细介绍所有细节)。有没有办法在GPU上更新这些缓冲区?我使用的是英特尔GPGPU,所以我共享内存,理论上可以做到这一点。

它变得令人沮丧,因为我一次又一次地在GPU上找到的速度增加,淹没了为每一帧设置所有内容的开销。

修改1:

我不认为我原来的代码示例真正展示了我做得不错的事情,所以我创建了一个真实世界的工作示例并将其发布在github here上。

由于遗留原因和时间原因,我无法更改当前产品的最重要架构。我试图&#34;跌入&#34;某些部分的GPU代码速度很慢,以加快速度。考虑到我所看到的限制,这可能是不可能的。但是,让我更好地解释一下我在做什么。

我会给出代码,但我会指的是函数&#34; ComputePoints&#34;在班级&#34; GPUComputePoints&#34;。

正如您在我的ComputePoints函数中看到的,每次传入CameraFrame以及转换矩阵M和b。

public static Point3D[] ComputePoints(CameraFrame frame, float[] M, float[] b)

这些是从我们的管道生成的新数组,而不是我可以留下的数组。所以我为每个人创建了一个新的ComputeBuffer:

       ComputeBuffer<ushort> inputBuffer = new ComputeBuffer<ushort>(_context,
          ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer,
          frame.RawData);
        _kernel.SetMemoryArgument(0, inputBuffer);

        Point3D[] ret = new Point3D[frame.Width * frame.Height]; 
        ComputeBuffer<Point3D> outputBuffer = new ComputeBuffer<Point3D>(_context,
            ComputeMemoryFlags.WriteOnly | ComputeMemoryFlags.UseHostPointer,
            ret);
        _kernel.SetMemoryArgument(1, outputBuffer);

        ComputeBuffer<float> mBuffer = new ComputeBuffer<float>(_context,
            ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer,
            M);
        _kernel.SetMemoryArgument(2, mBuffer);

        ComputeBuffer<float> bBuffer = new ComputeBuffer<float>(_context,
            ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer,
            b);
         _kernel.SetMemoryArgument(3, bBuffer);

......我认为,其中存在性能上的损失。有人提到,为了解决这个问题,请使用map / unmap功能。但是我没有看到这会有什么帮助,因为我每次都需要创建缓冲区来封装传入的新数组,对吧?

1 个答案:

答案 0 :(得分:0)

  

进入的像素数组对于每一帧都是不同的,所以我   必须为此创建一个新的ComputeBuffer。

您可以创建一个大缓冲区,然后将其范围用于多个不同的帧。然后你不必在每一帧重新创建(也不重新发布)。

  

当我们更新场景时,我们的矩阵也会定期更改   (再次,我不能详述所有细节)。

每个未使用的缓冲区用于N次迭代/帧,你可以释放,对于每个非足够的缓冲区存在,你可以释放最后一个缓冲区并重新创建2x更大的缓冲区以再次释放之前使用多次。< / p>

如果内核参数的数量和顺序保持不变,则不需要在每个帧都设置它们。

  

有没有办法在GPU上更新这些缓冲区?

对于opencl版本&lt; = 1.2(没有共享虚拟内存?),建议不要在主机端使用设备端指针或在设备端使用主机端指针

但如果它不与视频适配器或生成视频帧的任何内容(可能使用use_host_ptr)发生冲突,它可能会有效。

无需重新创建CommandQueue。创建一次,用于每个有序的工作。

如果由于类似于以下的软件设计而重新创建所有这些:

 float [] results = test(videoFeedData);

然后你可以尝试像

这样的东西
float [] results = new float[n];
test(videoFeedData,results);

因此它不需要创建所有内容,而是获取结果或输入数据的大小,然后创建opencl缓冲区一次,将其缓存在某个地方(如地图/字典),然后在采用类似大小的数组时重新使用。

实际工作如下:

new frame feed-0: 1kB data ---> allocate 1kB
feed-1: 10 MB data ---> allocate 10 MB, delete 1kB one
feed-2: 3 MB data ---> re-use 10MB one
feed-3: 2 kB data ---> re-use 10MB 
feed-4: 100 MB data ---> delete 10MB, allocate 100MB
feed-5: 110 MB data ----> delete 100MB, allocate 200MB
feed-6: 120 MB data  ---> re-use 200 MB
feed-7: 150 MB data  ---> re-use 200 MB 
feed-8: 90 MB data  ---> re-use 200 MB

输入和输出数据。

在实际重新创建的开销之上,重新创建许多东西会阻碍驱动程序优化和重置。

也许是这样的:

 CoresGpu gpu = new CoresGpu(kernelString,options,"gpu");

 for(i 0 to 100)
 {
   float [] results = new float[n];

   // allocate new, if only not enough, deallocate old, if only not used
   gpu.compute(new object[]{getVideoFeedBuffer(),brush21x21array,results},
             new string[]{"input","input","output"},
             kernelName,numberOfThreads);

   toCloudDb(results.toList());
 }

 gpu.release(); // everything is released here

如果必须重新创建,无法逃避它,那么你甚至可以使用流水线来隐藏重新创建的延迟(但仍然比完美更慢)。

push data
thread-0:get video feed

push data
thread-0:get next video feed
thread-1:send old video feed to gpu

push data
thread-0:get third video feed
thread-1:send second video feed to gpu
thread-2:compute on gpu

push data
thread-0:get fourth video feed
thread-1:send third video feed to gpu
thread-2:compute second frame on gpu
thread-3:get result of first frame from gpu to RAM

push data
thread-0:get fifth video feed
thread-1:send fourth video feed to gpu
thread-2:compute third frame on gpu
thread-3:get result of second frame from gpu to RAM
pop first data

...
...
pop second data

像这样继续使用:

var result=gpu.pipeline.push(videoFeed);
if(result!=null)
{ result has been popped! }

重新创建延迟的一部分被计算,复制,录像和弹出操作隐藏。如果重新创建是总时间的%90,则它将仅隐藏%10。如果它是%50,那么它会隐藏其他%50。

  

5)执行内核并等待响应。

为什么要等?框架是否相互绑定?如果没有,您也可以使用多个管道。然后,您可以在每个管道中同时重新创建多个缓冲区,这样可以完成更多的工作,但浪费的周期太多。使用大缓冲区可以最快。