OpenGL因重度计算而崩溃

时间:2017-02-23 17:06:18

标签: c++ opengl gpu mandelbrot

我是OpenGL的新手。我的第一个项目是渲染一个mandelbrot集(我觉得非常有趣),并且由于计算的性质必须要完成,我认为在GPU上做它们会更好(基本上我在每个上应用一个复杂的函数)复杂平面的一部分的点,很多时间,我根据输出着色这一点:许多可并行化的计算,这对GPU来说似乎不错,对吗?)。

所以当单个图像的计算量不是太多时,一切都运行良好但是一旦像素*迭代超过大约9亿,程序崩溃(显示的图像显示只有部分图像被计算出来,青色部分是初始背景):

Dark Part of the Mandelbrot Set not Fully Calculated

事实上,如果计算总数低于此限制但足够接近(比如85亿),它仍然会崩溃,但会花费更多时间。所以我想有一些问题没有出现在足够少的计算中(它一直运行完美,直到它到达那里)。我真的不知道它可能是什么,因为我对此真的很陌生。当程序崩溃时,它说:“Mandelbrot Set.exe中的0x000000005DA6DD38(nvoglv64.dll)处理未处理的异常:请求致命程序退出。”它也是那里指定的相同地址(它只在我退出Visual Studio,我的IDE时才会改变)。

这里是整个代码,加上着色器文件(顶点着色器没有做任何事情,所有计算都在片段着色器中): 编辑: 这是项目的所有.cpp和.h文件的链接,代码太大,无法放在这里,无论如何都是正确的(虽然远非完美); https://github.com/JeffEkaka/Mandelbrot/tree/master

以下是着色器:

NoChanges.vert(顶点着色器)

#version 400

// Inputs
in vec2 vertexPosition;  // 2D vec.
in vec4 vertexColor;

out vec2 fragmentPosition;
out vec4 fragmentColor;

void main() {
gl_Position.xy = vertexPosition;
gl_Position.z = 0.0;
gl_Position.w = 1.0;  // Default.

fragmentPosition = vertexPosition;

fragmentColor = vertexColor;

}

CalculationAndColorShader.frag(片段着色器)

#version 400
uniform int WIDTH;
uniform int HEIGHT;

uniform int iter;

uniform double xmin;
uniform double xmax;
uniform double ymin;
uniform double ymax;

void main() {
dvec2 z, c;

c.x = xmin + (double(gl_FragCoord.x) * (xmax - xmin) / double(WIDTH));
c.y = ymin + (double(gl_FragCoord.y) * (ymax - ymin) / double(HEIGHT));

int i;
z = c;
for(i=0; i<iter; i++) {
    double x = (z.x * z.x - z.y * z.y) + c.x;
    double y = (z.y * z.x + z.x * z.y) + c.y;

    if((x * x + y * y) > 4.0) break;
    z.x = x;
    z.y = y;
}

float t = float(i) / float(iter);
float r = 9*(1-t)*t*t*t;
float g = 15*(1-t)*(1-t)*t*t;
float b = 8.5*(1-t)*(1-t)*(1-t)*t;

gl_FragColor = vec4(r, g, b, 1.0);

}

我正在使用SDL 2.0.5和glew 2.0.0,我相信OpenGL的最新版本。代码已在Visual Studio(我相信的MSVC编译器)上编译,并启用了一些优化。另外,即使在我的gpu计算中我也在使用双打(我知道它们超慢,但我需要它们的精度)。

1 个答案:

答案 0 :(得分:4)

您需要了解的第一件事是&#34;上下文切换&#34; GPU(主要是大多数异构架构)与CPU /主机架构不同。当您向GPU提交任务时 - 在这种情况下,&#34;渲染我的图像&#34; - GPU将完成该任务直到完成。

我自然会抽象出一些细节:Nvidia硬件将尝试在未使用的内核上安排较小的任务,并且所有三个主要供应商(AMD,Intel,NVidia)都有一些微调的行为,使我的上述概括复杂化,但作为一个原则问题,您应该假设提交给GPU的任何任务将消耗GPU的全部资源,直到完成。

就其本身而言,这不是一个大问题。

但是在Windows(以及大多数消费者操作系统)上,如果GPU在单个任务上花费太多时间,操作系统将假设GPU没有响应,并且将执行多种不同的操作之一(或可能是其中多个的子集):

  • 崩溃:不再发生这么多了,但在较旧的系统上,我用过于雄心勃勃的Mandelbrot渲染器对我的计算机进行了蓝屏显示
  • 重置驱动程序:这意味着您将失去所有OpenGL状态,并且从程序的角度来看基本上无法恢复
  • 中止操作:一些较新的设备驱动程序非常聪明,可以简单地终止任务而不是终止整个上下文状态。但这可能取决于您使用的特定API:我的基于OpenGL / GLSL的Mandelbrot程序会导致驱动程序崩溃,而我的OpenCL程序通常会有更优雅的故障。
  • 让它完成,没有问题:如果操作系统没有将GPU用作显示驱动程序,则只会发生 / strong>即可。因此,如果您的系统中有多个图形卡并且明确确保在操作系统未使用的图形卡上进行渲染,或者正在使用的卡是计算卡可能没有与之关联的显示驱动程序。在OpenGL中,这基本上是一个非首发,但如果你使用OpenCL或Vulkan,这可能是一个潜在的解决方法。

准确的时间会有所不同,但您通常会认为如果单个任务的时间超过2秒,则会导致程序崩溃。

那么你如何解决这个问题呢?好吧,如果这是一个基于OpenCL的渲染,那将非常简单:

std::vector<cl_event> events;
for(int32_t x = 0; x < WIDTH; x += KERNEL_SIZE) {
    for(int32_t y = 0; y < HEIGHT; y += KERNEL_SIZE) {
        int32_t render_start[2] = {x, y};
        int32_t render_end[2] = {std::min(WIDTH, x + KERNEL_SIZE), std::min(HEIGHT, y + KERNEL_SIZE)};
        events.emplace_back();
        //I'm abstracting the clSubmitNDKernel call
        submit_task(queue, kernel, render_start, render_end, &events.back(), /*...*/);
    }
}

clWaitForEvents(queue, events.data(), events.size());

在OpenGL中,您可以使用相同的基本原理,但事情变得有点复杂,因为OpenGL模型的抽象程度是多么荒谬。因为驱动程序想要将多个绘制调用捆绑到一个命令到底层硬件,所以你需要明确地让它们自己运行,否则驱动程序会将它们全部捆绑在一起,你就会得到完全相同的问题即使你已经把它写成具体分解任务。

for(int32_t x = 0; x < WIDTH; x += KERNEL_SIZE) {
    for(int32_t y = 0; y < HEIGHT; y += KERNEL_SIZE) {
        int32_t render_start[2] = {x, y};
        int32_t render_end[2] = {std::min(WIDTH, x + KERNEL_SIZE), std::min(HEIGHT, y + KERNEL_SIZE)};
        render_portion_of_image(render_start, render_end);
        //The call to glFinish is the important part: otherwise, even breaking up 
        //the task like this, the driver might still try to bundle everything together!
        glFinish();
    }
}

render_portion_of_image的确切外观是您自己需要设计的内容,但基本的想法是为程序指定render_startrender_end之间的像素将被渲染。

您可能想知道KERNEL_SIZE应该是什么价值。这是你必须自己试验的东西,因为它完全取决于你的显卡有多强大。值应为

  • 足够小,任何单个任务都不会花费超过x的时间(我通常会持续50毫秒,但只要你保持低于半秒,它通常是安全的)
  • 足够大,以至于您无法向GPU提交数十万个小任务。在某个时刻,你将花费更多的时间来同步主机←→GPU接口而不是实际在GPU上工作,并且由于GPU架构通常有数百甚至数千个内核,如果你的任务太小,你&# 39;仅仅通过不使所有核心饱和来降低速度。

根据我的个人经验,确定的最佳方法是进行一系列测试&#34;在程序开始之前渲染,在Mandelbrot Set的中心灯泡的32x32图像上渲染10,000次迭代的图像(一次渲染,没有分解算法),并查看它有多长需要。我使用的算法基本上是这样的:

int32_t KERNEL_SIZE = 32;
std::chrono::nanoseconds duration = 0;
while(KERNEL_SIZE < 2048 && duration < std::chrono::milliseconds(50)) {
    //duration_of is some code I've written to time the task. It's best to use GPU-based 
    //profiling, as it'll be more accurate than host-profiling.
    duration = duration_of([&]{render_whole_image(KERNEL_SIZE)});
    if(duration < std::chrono::milliseconds(50)) {
        if(is_power_of_2(KERNEL_SIZE)) KERNEL_SIZE += KERNEL_SIZE / 2;
        else KERNEL_SIZE += KERNEL_SIZE / 3;
    }
}

final_kernel_size = KERNEL_SIZE;

我建议的最后一件事是使用OpenCL来重复提升渲染mandelbrot设置本身,并使用OpenGL(包括OpenGL←→OpenCL Interop API!)来实际显示屏幕上的图像。从技术层面来看,OpenCL既不比OpenGL更快也不慢,但是它可以让你对你执行的操作有很多控制,并且更容易推断出GPU正在做什么(以及当你使用比OpenGL更明确的API时,你需要做的就是改变它的行为。你可以,如果你想坚持使用单一的API,可以使用Vulkan,但由于Vulkan非常低级,因此使用非常复杂,除非你接受挑战,否则我不建议这样做

编辑:其他一些事情:

  • 我有多个版本的程序,一个用float呈现,另一个用double呈现。在我的此程序版本中,我实际上有一个版本使用两个float值来模拟double,如here所述。在大多数硬件上,这可能会更慢,但在某些架构(特别是NVidia的Maxwell架构)上,如果处理速度float足够快,它实际上可以仅仅通过纯粹的表现胜过double幅度:在某些GPU架构上,floatdouble快32倍。
  • 你可能想要有一个&#34;自适应&#34;动态调整内核大小的算法。这比它的价值更麻烦,在主机上重新评估下一个内核大小的时间将超过你实现的任何轻微的性能提升。