UPL9-17-d——组内线程数量

Compute Shader 中的执行模型

Compute Shader 的执行模型可分为三层结构

  • 第一层 —— 线程

    HLSL 函数内部自动生成,由 GPU 并行执行的最小计算单元,负责执行入口函数的一次调用

  • 第二层 —— 线程组

    Compute Shader 中由 [numthreads(x, y, z)] 定义,
    每个组内包含固定数量的线程,共享组内内存、可进行同步

  • 第三层 —— 线程组网格

    C# 侧 Dispatch() 调用,定义多少个组要被 GPU 启动,控制全局总线程数

相当于:

  1. GPU 线程会执行入口函数
  2. 入口函数前的 [numthreads(x, y, z)] 决定单个 线程组内 线程数量
  3. C# 侧调用 Dispatch​ 时传入的 x​、y​、z 决定要启动多少个 线程组

三个轴向的数字代表什么

  1. 入口函数前的 [numthreads(x, y, z)] 用于控制 每个线程组一共有多少个线程

    比如:[numthreads(8, 8, 1)]​,表示每个线程组有 8 * 8 * 1 = 64 个线程

  2. C# 侧调用 Dispatch​ 时传入的 x​、y​、z 决定一共有多少个线程组

    比如:入口函数:[numthreads(8, 8, 1)]​,C# 调用:Dispatch(kernel, 64, 64, 1);
    表示一共会开启 (64 * 8, 64 * 8, 1 * 1) = (512, 512, 1) = 512 * 512 * 1 = 262144 个线程

也就是说他们两共同决定了线程总数:

1
2
3
总线程数 = numthreads(x,y,z) * Dispatch(groupsX,groupsY,groupsZ)
= (groupsX * x, groupsY * y, groupsZ * y)
= groupsX * x * groupsY * y * groupsZ * y

注意:每个轴的线程或线程组数不能设置为 0,最小为 1,即 x, y, z >= 1

  • 如果是 1D 任务(一维数据,比如处理线性数组计算):x,1,1
  • 如果是 2D 任务(二维数据,比如处理图像、纹理等数据):x, y, 1
  • 如果是 3D 任务(三维数据,比如处理体积型数据,3D 纹理、网格等数据):x, y, z

数值规则

  1. [numthreads(x, y, z)] 的规则

    三个参数都是常量整数,取值范围:

    1. 每个维度 >= 1
    2. 每个维度 <= 1024
    3. x * y * z <= 1024

    常见组合:(8, 8, 1)​、(16, 16, 1)​、(32, 1, 1)

  2. Dispatch(groupsX, groupsY, groupsZ) 的规则

    三个参数都是整数,取值范围:

    1. 每个维度 ≥ 1,不能为 0
    2. 没有硬性上限(取决于 GPU 支持的最大 Dispatch 数量),但通常非常大(几十万没问题)

匹配规则

当我们要处理二维纹理、数组、体积数据时,应该让线程数覆盖整个数据区域
一般情况下,线程数和分组数的公式如下:

1
2
3
int groupsX = Mathf.CeilToInt(width / (float)numThreadX);
int groupsY = Mathf.CeilToInt(height / (float)numThreadY);
int groupsZ = Mathf.CeilToInt(depth / (float)numThreadZ);

其中:

  • groups​:Dispatch 传入的值
  • numThreadX​:numthreads 传入的值
  • width​、height​、depth:代表你要处理的数据维度数量
  • CeilToInt​:向上取整的目的是为了即使不是整数倍也能完全覆盖,超出部分的线程可以在 HLSL 中用边界判断过滤掉

举例说明:比如我们要利用 Compute Shader 处理一个 512 * 512 像素的二维图像,我们需要在大于等于 512 * 512 个线程去处理这个图像

  1. Compute Shader 的 numthreads​ 为:[numthreads(8, 8, 1)]

  2. 那么在 C# 侧:

    1
    2
    3
    int groupsX = Mathf.CeilToInt(512f / 8) = 64;
    int groupsY = Mathf.CeilToInt(512f / 8) = 64;
    Dispatch(kernel, groupsX, groupsY, 1);
  3. 实际启动的线程为:(64 * 8, 64 * 8, 1 * 1) = 512 * 512 = 262144

推荐规则

  • 1D 数据(例如:数组):numthreads​ - (64,1,1)​ 或 (128,1,1)
  • 2D 数据(例如:贴图):numthreads​ - (8,8,1)​ 或 (16,16,1)
  • 3D 数据(例如:网格):numthreads​ - (4,4,4)​ 或 (8,8,8)

主要原因:GPU 线程调度单位是 Warp(英伟达) / Wavefront(AMD)(32 或 64 个线程)
所以最好让 x * y * z 是 32 或 64 的倍数

总结

numthreads​ 和 Dispatch​ 中的 xyz 三个轴
numthreads​ 定义线程组的大小(最多 1024 个线程)
Dispatch​ 定义要开启多少个组,一般按 数据尺寸 / numthreads 向上取整计算
总线程数 = 所有线程组的线程的乘积,因此所有维度都必须 ≥1

  • 1D:处理线性数据(数组、粒子)
  • 2D:处理平面数据(图像、屏幕)
  • 3D:处理体积数据(3D 纹理、网格)