Occa: 新的内核限定符宏

创建于 2020-04-02  ·  22评论  ·  资料来源: libocca/occa

为内核编译器提供一些关于线程块(工作组)中线程(工作项)数量的额外信息通常很有帮助。 例如,我们可以给 HIP 编译器一个线程块中线程数的上限(比如 1024),如下所示:

__launch_bounds__(1024) __global__ void fooKernel(...) { ... }

事实上,对于当前的 HIP 版本,不幸的是,当线程块大小超过 256 时必须指定这一点(参见 https://github.com/ROCm-Developer-Tools/HIP/issues/1310 )

CUDA 也具有相同的属性。 对于最小线程块数(https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#launch-bounds),启动边界限定符还有一个额外的参数。

OpenCL 中的内核限定符略有不同(参见 https://www.khronos.org/registry/OpenCL/specs/2.2/pdf/OpenCL_C.pdf 的 6.7.2)

建议 v1 - 在我们知道编译时线程块大小的理想世界中,OCCA 将为适当的启动边界(CUDA、HIP)或工作组大小提示(OpenCL)添加内核限定符。

Proposal v2 - 如果 Proposal v1 太复杂,那么最好为启动边界添加一个 okl 属性 @qualifier("inner sizes", B) 其中 B 可以是编译器定义。 这将扩展到 __launch_bounds__(value of B) 用于 CUDA/HIP 或 __attribute__((work_group_size_hint(value of B))) 用于 OpenCL。 多暗变体也会有所帮助。

feature

所有22条评论

由于这似乎特定于 HIP/CUDA/OpenCL,那么将其作为构建属性传递呢?

  addVectors = device.buildKernel("addVectors.okl",
                                  "addVectors",
                                  "launch_bounds: 1024");

我想那会很好。

我刚碰到这个。 我很高兴为此做出贡献,但我有一个问题。 OCCA 是否知道 JIT 时间的内循环尺寸?

我相信循环维度可以作为内核参数传入。

这是真的,但我不明白为什么这很有用。 我在问 occa 如何在 JIT 时间发出__launch_bounds__属性。 它需要知道此时的循环尺寸。 这与内核参数正交,不是吗?

他说内部循环的暗淡可以作为参数传递,即我们不一定知道 JIT 编译时的线程块尺寸。

啊对。 我明白你现在在说什么@tcew。 好点子。

这就是我的想法:

<strong i="6">@kernel</strong> void runtimeArgs(const int B,
                         const int T,
                         const int N,
                         const float *x,
                         const float *y,
                         float *xPy) {
  for (int b=0;b<B;++b;@outer(0)){
    for (int t=0;t<T;++t;@inner(0)){

      if(b==0 && t==0) printf("B=%d, T=%d\n", B, T);

      int  n = t + T*b;
      if(n<N){
        xPy[n] = x[n] + y[n];
      }
    }
  }
}

OCCA 显然无法知道 JIT 时间的数值循环边界。

但是,它确实创建了一个设置线程网格尺寸的启动器:

extern "C" void runtimeArgs(occa::modeKernel_t * *deviceKernel,
                            const int & B,
                            const int & T,
                            const int & N,
                            occa::modeMemory_t * x,
                            occa::modeMemory_t * y,
                            occa::modeMemory_t * xPy) {
  {
    occa::dim outer, inner;
    outer.dims = 1;
    inner.dims = 1;
    int b = 0;
    outer[0] = B - 0;
    int t = 0;
    inner[0] = T - 0;
    occa::kernel kernel(deviceKernel[0]);
    kernel.setRunDims(outer, inner);
    kernel(B, T, N, x, y, xPy);
  }
}

因此,用户可以在运行时为循环边界指定任何大小。

交叉的帖子。

SYCL 使用类似的语法:

sycl::range<2> global_range(Bx*Tx,By*Ty);
sycl::range<2> local_range(Tx,Ty);
sycl::nd_range<2> kernel_range(global_range, local_range);

device_queue.submit([&](sycl::handler &cgh) {
    ...
  cgh.parallel_for(kernel_range, kernel);
});

(缓冲区有一个与之关联的范围,这避免了传入 N 的需要)

SYCL sycl::range语法改编自 OpenCL,OpenCL 本身改编自 CUDA。

在您的示例中,线程尺寸的规范与并行 for 循环的主体分开。

OCCA OKL 语法专门设计用于将循环维度和主体代码带入更熟悉的并行 for 循环语法。

for (int b=0;b<B;++b;@outer(0)){ /*  grid dimension defined here */
    for (int t=0;t<T;++t;@inner(0)){ /* thread block dimension defined here */

      if(b==0 && t==0) printf("B=%d, T=%d\n", B, T);

      int  n = t + T*b;
      if(n<N){
        xPy[n] = x[n] + y[n];
      }
    }
  }

内核内部的代码应该使并行 for 循环边界与并行 for 循环的主体非常接近。 此外,任何输入数组都未指定循环边界,因为更通用的内核可能需要与数据数组非常不同的线程网格配置。

OKL 内核结构是一种有意的选择,源于在培训人员时必须反复解释 CUDA/OpenCL 内核语法、内核启动参数和内核线程原理。

SYCL 是否有类似的线程块大小提示,例如我们在这里讨论的 CUDA/HIP 的__launch_bounds__

好问题。 我仔细检查了 SYCL 标准 (v1.2.1) 以找出答案。 支持 OpenCL C 中可用的任何属性,并且可以使用 C++ 11 属性说明符使用cl命名空间给出。 例如,OpenCL C 中的__attribute__(((reqd_work_group_size(n)))等价于 SYCL 中的[[cl::reqd_work_group_size(n)]]

有两种方式可用于指定线程组大小: work_group_size_hint(n)是软版本——建议线程组大小将是n ——而req_work_group_size(n)是严格要求.

OCCA 的一些选项:

  1. 向 occa::kernel 类添加“innerDimHint”成员函数,强制重新编译(如果尚未在哈希中),并为 CUDA/OpenCL/HIP 提供线程暗淡提示。

  2. 在启动器中添加一些逻辑,以在指定新的线程数组大小时触发重新编译。 某些 OCCA_ *定义可能会打开/关闭此功能。

这两者都可以以向后兼容的方式完成。

我实际上会相信单独的运行时来管理它,并选择不重新编译任何东西。 本质上,使用@dmed256的原始提议,将其作为构建道具,然后在翻译时将相应的__launch_bound__提示添加到内核(如果后端支持)。

对于 CUDA 和 HIP, __launch_bound__实际上只是一个提示,所以可能更类似于 OpenCL 的work_group_size_hint 。 它仅用于告诉编译器它可以假设有多少寄存器可供块中的每个线程使用。 如果用户违反了启动界限,则不一定是错误,因为内核可能没有大量使用寄存器。 如果用户违反了启动限制并且确实没有足够的寄存器,运行时将抛出一个 OCCA 应该捕获的错误。

在运行自动调谐器时,我注意到需要太多 LDS 或 REG 的OCCA:HIP内核的分段错误。 我怀疑 HIP 会抛出将被捕获的错误。 希望现在已经解决了。

我可以接受用户提供的提示,或者在运行时发现新线程配置的启动器。

仅供参考,我很高兴通过我传递的 OKL 内核代码中的宏常量替换(例如, K_blockDim用于外部, K_threadDim用于内部)从主机代码中指定显式运行时循环边界通过内核道具。 当然,我有自己的 API 代码查询 GPU 的特性和状态。 我使用该信息来计算要传递的循环边界。 是的,这种动态的东西有时会在运行时导致 JIT 重新编译,但在我的情况下,这种情况很少见,因为我的步阈值发生了变化,所以实际的循环边界通常保持不变或落入给定内核的某个公共集合中。 我还为我的一些内核使用了预编译,这样也减少了 JIT 重新编译。

在运行时显式定义这些循环边界还有助于在 OKL 代码中使用它们来调整 GPU 本地内存数组的运行时大小。

看起来这个功能很重要:
https://rocmdocs.amd.com/en/latest/Current_Release_Notes/Current-Release-Notes.html#performance -impact-for-kernel-launch-bound-attribute

对笨拙的实现表示歉意,这是我用于 libparanumal 中需要 Np (>256) 线程的内核的解决方法:

occa::properties kernelInfo; 
...
 if(platform.device.mode()=="HIP"){
      char newflag[BUFSIZ];
      sprintf(newflag, " --gpu-max-threads-per-block=%d", mesh.Np);
      kernelInfo["compiler_flags"] += newflag;
    }

Noel Chalmers 建议使用 hipcc 编译器标志来指定启动边界。 总体实施是我的。

执行此操作时务必小心,因为不清楚如果内核违反最大界限会发生什么。

为了避免使用不适当的界限,我为使用不同最大线程数的内核创建了occa::properties对象的单独副本。

将来,违反启动界限很可能会成为运行时错误。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

amikstcyr picture amikstcyr  ·  11评论

awehrfritz picture awehrfritz  ·  7评论

jeremylt picture jeremylt  ·  12评论

dmed256 picture dmed256  ·  4评论

tcew picture tcew  ·  10评论