【问题标题】:Kernel slower on newer and "better" Nvidia GPUs在更新和“更好”的 Nvidia GPU 上内核变慢
【发布时间】:2016-04-18 06:37:56
【问题描述】:

我在 OpenCL 中创建了一个实时光线追踪器。这是在 GTX 580 上开发的。我停止了几年的工作,最近又将它复活了。我预计使用更新和“更好”的 Nvidia GPU 时它会运行得更快。但是,它仍然在 GTX 580 上运行得最快。

这是我在三台不同的计算机和显卡上使用的基准场景的时间表

GPU          Kernel time    CPU                     OS             System Mem
GTX 580      11 ms          E5-1670                 Windows 7      32 GB
GTX Titan    15 ms          W5580 (two processors)  Windows 7      48 GB
GTX 980M     15 ms          i7-4710HQ (laptop)      Windows 10     16 GB

每台计算机都在 2016 年 1 月 10 日安装了 Nvidia 驱动程序 361.43,并且主机代码使用 Visual Studio 2013 64 位发布模式编译。

我还观察到 GTX 580 的帧速率更快。

我用过

time_end = clevent.getProfilingInfo<CL_PROFILING_COMMAND_END>();
time_start = clevent.getProfilingInfo<CL_PROFILING_COMMAND_START>();

获取内核时间。我不使用双浮点扩展 (//#pragma OPENCL EXTENSION cl_khr_fp64 : enable)。

内核代码被分解成几个内核文件,我将它们组装成一个文件,它有几千行代码。

为什么我的内核在更新和“更好”的硬件上会变慢?


这是我创建上下文的代码。这并不完全有意义,但总比没有好

void Contexts::init(string sourceCode) {
    run_time = -1;
    context = createCLContext(type, vendor);

    cl_uint uiNumSupportedFormats = 0;

    devices = context.getInfo<CL_CONTEXT_DEVICES>();
    int err = 0;
    try{
        //queues.push_back(cl::CommandQueue(context, devices[i], 0, &err));
        //queue = cl::CommandQueue(context, devices[device], CL_QUEUE_PROFILING_ENABLE, &err);
        queue = cl::CommandQueue(context, devices[device], CL_QUEUE_PROFILING_ENABLE|CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE, &err);
        //printf("\t\tDevice: %s\n", devices[device].getInfo<CL_DEVICE_NAME>().c_str());
    }
    catch (cl::Error er) {
        printf("ERROR: %s(%d)\n", er.what(), er.err());
    }


    //ndevices = devices.size();
    //if(ndevices>max_devices) ndevices = max_devices;

    program = buildProgramFromSource(context, sourceCode);

    try{
        kernel1 = cl::Kernel(program, "trace", &err);
        kernel2 = cl::Kernel(program, "transform_primitives", &err);
        kernel_postprocess = cl::Kernel(program, "post_process", &err);
    }
    catch (cl::Error er) {
        printf("ERROR: %s(%d)\n", er.what(), er.err());
    }
}

cl::Buffer Contexts::copy_buffer(int size, const void* ptr, int flags = CL_MEM_READ_ONLY) {
    cl::Buffer out;
    if(size>0) {
        out = cl::Buffer(context, flags| CL_MEM_COPY_HOST_PTR,  size, (void*)ptr);
    }
    else {
        //NULL pointers to kernel do not seem to work on INTEL so use this hack
        out = cl::Buffer(context, flags,  1, NULL);
    }
    return out;
}
void Contexts::copy_buffers() {
    //int cubemap_size = para->cubemap->sizeX * para->cubemap->sizeY * 6 * para->cubemap->ncubemap;

    //if(para->cubemap->sizeX== -1) cubemap_size = 0;
    int nobj = para->kernel1_parameters.nobj;
    int nprim = para->kernel1_parameters.nprim;
    int nmat= para->kernel1_parameters.nmat;
    int nlight = para->kernel1_parameters.nlight;
    int nnode = para->kernel1_parameters.nnode;
    int nmap = para->nmaps;

    int err = 0;

    int npixels = para->kernel1_parameters.height*para->kernel1_parameters.width;

    int exposure_samples = para->kernel1_parameters.exposure_samples;

    int mask_size = para->kernel1_parameters.mask_size;
    int nmask = (2*mask_size+1)*(2*mask_size+1);

    cl_objects_mem = copy_buffer(sizeof(CSG_object)*nobj, para->objects);
    cl_node_mem = copy_buffer(sizeof(Node)*nnode, para->nodes);
    cl_prim_mem = copy_buffer(sizeof(Primitive)*nprim, para->prims, CL_MEM_READ_WRITE);
    cl_light_mem = copy_buffer(sizeof(Light)*nlight, para->lights);
    cl_mat_mem = copy_buffer(sizeof(Material)*nmat, para->mats);
    cubemap_info = copy_buffer(sizeof(Cubemap_info)*nmap, para->maps);
    cubemap_images = copy_buffer(sizeof(cl_uchar4)*para->envmap_npixels, para->envmap_images);
    cl_mask_mem = copy_buffer(sizeof(cl_float)*nmask, para->mask);

    cl_image_mem = cl::Buffer(context, CL_MEM_WRITE_ONLY, sizeof(cl_uchar4)*npixels, NULL, &err);
    cl_results_mem = cl::Buffer(context, CL_MEM_READ_WRITE, sizeof(cl_float4)*npixels, NULL, &err);
    cl_luminance = cl::Buffer(context, CL_MEM_WRITE_ONLY, sizeof(cl_float)*exposure_samples, NULL, &err);

    if(para->surfacecpy_sw) {
        cmPinnedBufOut1 = cl::Buffer(context, CL_MEM_WRITE_ONLY |CL_MEM_ALLOC_HOST_PTR, sizeof(cl_uchar4)*npixels, NULL, NULL);
        image = (int*)queue.enqueueMapBuffer(cmPinnedBufOut1, CL_TRUE, CL_MAP_READ, 0, sizeof(cl_uchar4)*npixels, 0, NULL, NULL);
        //queue.enqueueUnmapMemObject(cmPinnedBufOut1, image);
        //int pageSize = 4096;
        //image = (int*) _aligned_malloc(sizeof(cl_uchar4)*npixels, pageSize);
        //CL_MEM_USE_PERSISTENT_MEM_AMD
    }
    cmPinnedBufOut2 = cl::Buffer(context, CL_MEM_WRITE_ONLY |CL_MEM_ALLOC_HOST_PTR, sizeof(cl_float)*exposure_samples, NULL, NULL);
    luminance = (float*)queue.enqueueMapBuffer(cmPinnedBufOut2, CL_TRUE, CL_MAP_READ, 0, sizeof(cl_float)*exposure_samples, 0, NULL, NULL);

    queue.finish();
    //int kindex = 0;
    kernel1.setArg(0, cl_objects_mem);
    kernel1.setArg(1, cl_node_mem);
    kernel1.setArg(2, cl_prim_mem);
    kernel1.setArg(3, cl_mat_mem);
    kernel1.setArg(4, cl_light_mem);
    kernel1.setArg(5, cubemap_info);
    kernel1.setArg(6, cubemap_images);
    kernel1.setArg(7, cl_results_mem);

    kernel_postprocess.setArg(0, cl_results_mem);
    kernel_postprocess.setArg(1, cl_luminance);
    kernel_postprocess.setArg(2, cl_image_mem);
    kernel_postprocess.setArg(3, cl_mask_mem);

    kernel2.setArg(0, cl_prim_mem);

}

void Contexts::run() {
    int nprim = para->kernel2_parameters.nprim;
    cl_float speed = para->kernel2_parameters.speed;
    cl_float4 speed_obj = para->kernel2_parameters.speed_obj;
    cl_float16 cl_viewTransform;
    for(int i=0; i<16; i++)
        cl_viewTransform.s[i] = para->viewTransform[i];
    //para->kernel1_parameters.offset = offset;
    //para->kernel1_parameters.offset2 = offset2;

    kernel1.setArg(8, cl_viewTransform);
    kernel1.setArg(9, para->kernel1_parameters);
    kernel1.setArg(10, offset);

    kernel_postprocess.setArg(4, para->kernel1_parameters);
    kernel_postprocess.setArg(5, offset);
    kernel_postprocess.setArg(6, offset2);

    //kernel1.setArg(11, offset2);
    cl::NDRange local_size = cl::NDRange(local_work_size);
    if(local_work_size == 0) {
        local_size = cl::NullRange;
    }
    queue.enqueueNDRangeKernel(kernel1, cl::NullRange, cl::NDRange(size),  local_size, NULL, &clevent);
    queue.finish();
    cl_ulong time_start, time_end;

    time_end = clevent.getProfilingInfo<CL_PROFILING_COMMAND_END>();
    time_start = clevent.getProfilingInfo<CL_PROFILING_COMMAND_START>();

    run_time = (float)(time_end - time_start);

    //post_process
    queue.enqueueNDRangeKernel(kernel_postprocess, cl::NullRange, cl::NDRange(size),  local_size, NULL, &clevent);
    queue.finish();

    time_end = clevent.getProfilingInfo<CL_PROFILING_COMMAND_END>();
    time_start = clevent.getProfilingInfo<CL_PROFILING_COMMAND_START>();

    run_time += (float)(time_end - time_start);
    //printf("run time %f, run time2 %f\n", run_time, run_time2);

    //kernel2
    kernel2.setArg(1, speed);
    kernel2.setArg(2, speed_obj);
    queue.enqueueNDRangeKernel(kernel2, cl::NullRange, cl::NDRange(nprim), cl::NullRange, NULL, &clevent);
    queue.finish();

    time_end = clevent.getProfilingInfo<CL_PROFILING_COMMAND_END>();
    time_start = clevent.getProfilingInfo<CL_PROFILING_COMMAND_START>();

    run_time += (float)(time_end - time_start);

    if(para->getoutput_sw) {
        if(!para->surfacecpy_sw) {
            if(SDL_MUSTLOCK(para->surface)) {
                if(SDL_LockSurface(para->surface) < 0) return;
            }
            queue.enqueueReadBuffer(cl_image_mem, CL_TRUE, 0, sizeof(cl_uchar4)*size, (int*)para->surface->pixels + offset, NULL, &clevent);
            queue.finish();
            if(SDL_MUSTLOCK(para->surface))
                SDL_UnlockSurface(para->surface);
        }
        else {
            queue.enqueueReadBuffer(cl_image_mem, CL_TRUE, 0, sizeof(cl_uchar4)*size, (int*)image, NULL, &clevent);
            queue.finish();
        }
        queue.enqueueReadBuffer(cl_luminance, CL_TRUE, 0, sizeof(cl_float)*size2, luminance, NULL, &clevent);
        queue.finish();
    }
}

【问题讨论】:

  • 你指望我们瞎猜吗?硬件变得更加强大,但我们不知道您的示踪剂是什么样子或它是如何工作的。你的基准时间看起来也很小,似乎在毫秒范围内它会受到太多随机波动的影响,无法准确判断实际性能。
  • 我意识到我可能没有提供足够的信息来回答这个问题。但也许这是 OpenCL 和 Nvidia 的一个已知问题?我可能会很快发布我的代码,但它非常复杂。时间差异不是统计波动。我以每秒多帧的速度运行(在 GTX 580 上接近 90 FPS),我引用的是平均值。帧率明显不同。场景可以是动态的(移动对象),但对于基准测试,场景是静态的。
  • “GPU 更好”这是一个疯狂的猜测,一切都可能不会更好。如果 GPU A 有 100 个组,GPU B 有 1000 个组,但内存总线相同。然后在总线上同时进行分散读取将使 GPU B 而不是 GPU A 崩溃。这一切都归结为您正在使用的内核,以及它是否足够通用以适应并获得新 GPU 的额外性能。跨度>
  • @DarkZeros,我将不得不再次跳入其中并为每个 GPU 编写一些测试。这将需要一些时间,而我已经有一段时间没有了。
  • GTX 580 有 16 个 smx(独立)单元。 GTX 980m 只有 12 个。GTX580 着色器频率为 1544 MHz。当所有光线都做同样的事情时,gtx980m 会赢。 GTX 580 ---> 每秒更多 GB 带宽。

标签: opencl gpgpu nvidia raytracing


【解决方案1】:

您是否启用了乱序处理?在进行基本图像处理时,我在 Nvidia GPU 中遇到过类似的问题?

当您按顺序创建命令队列时,代码运行速度较慢,但​​如果您在 OpenCL 中创建的命令队列乱序(这在 Nvidia GPU 中是可能的),则执行速度会呈指数级增长。

参考 API

cl_command_queue clCreateCommandQueue( cl_context context, cl_device_id 设备, cl_command_queue_properties 属性, cl_int *errcode_ret)

https://www.khronos.org/registry/cl/sdk/1.0/docs/man/xhtml/clCreateCommandQueue.html

$cl_command_queue_properties 应设置为 $CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE

但请确保您的内核中没有数据依赖项,因为在这种情况下您不能使用此选项。

此外,请确保您查询计算单元的数量并相应地提供您的全局工作大小和本地工作大小。

例如我的 Nvidia GPU 有 4 个计算单元,因此为了获得最佳性能,我的全局工作大小应该能被 4 整除,我的本地工作大小应该是 4 的整数倍

【讨论】:

  • 谢谢。我正在使用CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE
  • 检查你是如何在内核中使用全局/本地内存的,如果你的内核进行了大量的全局内存访问,它仍然会变慢。我真的无法理解为什么它在更好的 gpu 中会更慢
  • 你提出了一个很好的观点。我可以发布用于创建上下文和运行内核的代码。那不是太长。几年来我没有对 OpenCL 进行大量编程。
  • 你的回答对我有帮助。我应该发布我是如何为内核创建上下文并获得输出的。我现在这样做了。我应该清理它,因为很多可能没有意义。至于全局大小和局部大小,我想我只是将全局大小设置为像素数,但我需要研究一下。
  • 你也可以只运行一个设备查询并发布你的opencl设备的规格吗,网上有很多设备查询代码......
【解决方案2】:

(在我的头顶)

CUDA/PTX 可以生成 32 位或 64 位。

OpenCL 编译器默认生成:

  • 计算能力 2.0 -> 32 位 ptx
  • 计算能力 3.0 及更高版本 -> 64 位 ptx

您的 GPU 是:

  • GTX 580 -> 计算能力 2.0
  • GTX Titan -> 计算能力 3.5
  • GTX 980M -> 计算能力 5.2

您可以输出生成的 ptx 以进行仔细检查,在没有 ptx 知识的情况下,应该很明显 ptx 代码是 32 位还是 64 位以及是哪种计算能力。

由于切换到 64 位 ptx,您可能会遇到更高的寄存器使用率 - 查看 CUDA 占用计算器以检查是否预期会变慢。如果这证实了,那么您将需要微调您的内核。

【讨论】:

    【解决方案3】:

    我无法提供具体答案 - GTX 580 和 GTX 980 之间的流式多处理器设计发生了重大变化。至少,您可能需要找到一个新的最佳本地和全局工作组规模。

    我的建议是使用 NVIDIA 的分析工具,因为它们仍然适用于 OpenCL。查看@jrprice 的post 以获取详细说明。记录分析数据后,您可以将其导入 Visual Profiler 并检查内核的寄存器和本地内存使用情况和占用情况。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-19
      • 1970-01-01
      • 1970-01-01
      • 2011-02-26
      • 1970-01-01
      • 2015-09-09
      • 2021-08-16
      • 2022-06-25
      相关资源
      最近更新 更多