使用多个GPU OpenCL

时间:2014-11-05 19:18:41

标签: opencl gpgpu

我有一个循环,在其中我将多个内核启动到GPU上。以下是摘录:

for (int idx = start; idx <= end ;idx ++) {

            ret = clEnqueueNDRangeKernel(command_queue, memset_kernel, 1, NULL,
                                            &global_item_size_memset, &local_item_size, 0, NULL, NULL);
            ASSERT_CL(ret, "Error after launching 1st memset_kernel !");


            ret = clEnqueueNDRangeKernel(command_queue, cholesky_kernel, 1, NULL,
                                                    &global_item_size_cholesky, &local_item_size, 0, NULL, NULL);
            ASSERT_CL(ret, "Error after launching 1st cholesky_kernel !");


            ret = clEnqueueNDRangeKernel(command_queue, ckf_kernel1, 1, NULL,
                                            &global_item_size_kernel1, &local_item_size, 0, NULL,  NULL);
            ASSERT_CL(ret, "Error after launching ckf_kernel1[i] !");



            clFinish(command_queue);
            ret = clEnqueueNDRangeKernel(command_queue, memset_kernel, 1, NULL,
                                            &global_item_size_memset, &local_item_size, 0, NULL, NULL);
            ASSERT_CL(ret, "Error after launching 2nd memset_kernel !");


            ret = clEnqueueNDRangeKernel(command_queue, cholesky_kernel, 1, NULL,
                                                    &global_item_size_cholesky, &local_item_size, 0, NULL, NULL);
            ASSERT_CL(ret, "Error after launching 2nd cholesky_kernel !");


            ret = clSetKernelArg(ckf_kernel2, 4, sizeof(idx), (void *)&idx);

            ret = clEnqueueNDRangeKernel(command_queue, ckf_kernel2, 1, NULL,
                                            &global_item_size_kernel2, &local_item_size, 0, NULL, NULL);
            ASSERT_CL(ret, "Error after launching ckf_kernel2 !");

现在,我想将此代码用于具有多个GPU的系统。所以我完成了以下步骤:

  • 为所有GPU创建了一个上下文。
  • 为每台设备创建一个命令队列。
  • 为每个设备创建单独的内核(下面的代码片段假设有两个gpus)
  • 为每个设备分配了单独的设备缓冲区

    cl_kernel ckf_kernel1[2];
    cl_kernel ckf_kernel2[2];
    cl_kernel cholesky_kernel[2];
    cl_kernel memset_kernel[2];
    
    // read get kernel.
    ckf_kernel1[0] = clCreateKernel(program, "ckf_kernel1", &ret);
    ASSERT_CL(ret, "Cannot load ckf_kernel1[i]!");
    ckf_kernel2[0] = clCreateKernel(program, "ckf_kernel2", &ret);
    ASSERT_CL(ret, "Cannot load ckf_kernel2!");
    memset_kernel[0] = clCreateKernel(program, "memset_zero", &ret);
    ASSERT_CL(ret, "Cannot load memset_kernel!");
    cholesky_kernel[0] = clCreateKernel(program, "cholesky_kernel", &ret);
    ASSERT_CL(ret, "Cannot load cholesky_kernel!");
    
    ckf_kernel1[1] = clCreateKernel(program, "ckf_kernel1", &ret);
    ASSERT_CL(ret, "Cannot load ckf_kernel1[i]!");
    ckf_kernel2[1] = clCreateKernel(program, "ckf_kernel2", &ret);
    ASSERT_CL(ret, "Cannot load ckf_kernel2!");
    memset_kernel[1] = clCreateKernel(program, "memset_zero", &ret);
    ASSERT_CL(ret, "Cannot load memset_kernel!");
    cholesky_kernel[1] = clCreateKernel(program, "cholesky_kernel", &ret);
    ASSERT_CL(ret, "Cannot load cholesky_kernel!");
    

现在,我不确定如何将内核启动到循环内的不同设备上。如何让它们并行执行?请注意,上面的循环中有一个clFinish命令。

另一个问题:在主机上使用多个线程/进程是标准做法,每个线程/进程负责在单个GPU上启动内核吗?

1 个答案:

答案 0 :(得分:3)

  1. 您无需为所有设备创建单独的上下文。如果他们来自不同的平台,你只需要这样做。
  2. 您也不需要创建单独的内核。您可以同时为多个设备编译内核(clBuildProgram支持多设备编译),如果在设备上启动内核,运行时将知道内核实体是否保持设备二进制对给定设备有效。
  3. 最简单的事情是:创建一个上下文,获取所需的所有设备,然后放入一个数组,然后使用该数组构建内核,并为其中的每个设备创建一个command_queue。
  4. clEnqueueNDRange内核是非阻塞的。你的for循环没有突破的唯一原因是因为clFinish()状态,并且很可能是因为你在订单队列中使用,这意味着单个设备的情况也可以在没有clFinish的情况下正常工作。
  5. 在OpenCL中最佳使用多GPU的一般想法是按照我提到的方式创建上下文内核队列,并使队列无序。这样,如果命令不具有未满足的依赖性,则允许命令并行执行,例如。 command2的输入不是command1的输出,然后它可以自由地开始与command1并行执行。但是,如果您正在使用此方法,则必须使用最后几个参数来clEnqueueNDRangeKernels,因为您必须使用cl_events构建此依赖关系链。每个clEnqueueWhatever可以等待一系列事件,这些事件源自其他命令。只有在满足所有依赖关系后,才会在队列中执行命令。

    有一个问题没有提及,那就是缓冲区的概念。如果要运行多GPU,则需要单独为设备显式创建缓冲区,并对数据进行分区。在2个设备上设置相同的缓冲区作为参数是无效的,而它们都试图写入它。充其量,运行时将序列化您的工作,并且2个设备将无法并行工作。这是因为缓冲区是内存的句柄,运行时负责将缓冲区的内容移动到需要它的设备。 (这可以隐式发生(延迟内存移动),或者在调用clEnqueueMigrateBuffer时显式发生。)禁止运行时同时为2个设备提供带有CL_MEM_READ_WRITE或CL_MEM_WRITE_ONLY标志的相同缓冲区。即使你知道程序员,2个设备可能没有写入缓冲区的相同部分,但运行时却没有。你必须告诉它。优雅的方法是创建2个子缓冲区,它们是较大/原始缓冲区的一部分;不太优雅的方法是简单地创建2个缓冲区。第一种方法更好,因为更容易从多个设备收集数据回主机,因为您只需要获取大缓冲区,运行时将知道哪些子缓冲区已在哪些设备上被修改,并且它将需要关心收集数据。

    如果我看到你的clSetKernelArgument调用,以及你正在使用的缓冲区,我可以看到你的内核依赖于什么,并写出你需要做什么,但我认为这对你来说是一个相当好的开始 - 设备运行。最终,它完全与数据有关。 (并开始使用无序队列,因为它有可能更快,并且它会强制您开始使用事件,这使得您和任何读取代码的人都可以明确表示,允许哪些内核并行运行。