100.ComputeShader组内线程数量

100.性能优化-GPU-着色器优化-ComputeShader-组内线程数量


100.1 知识点

Compute Shader 中的执行模型

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

  1. 第一层 —— 线程

    • HLSL 函数内部自动生成
    • 由 GPU 并行执行的最小计算单元
    • 负责执行入口函数的一次调用
  2. 第二层 —— 线程组

    • 在 Compute Shader 中由 [numthreads(x, y, z)] 定义
    • 每个组内包含固定数量的线程
    • 共享组内内存、可进行同步
  3. 第三层 —— 线程组网格

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

相当于:

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

三个轴向的数字代表什么

[numthreads(x, y, z)] 用于控制每个线程组一共有多少个线程。比如:

[numthreads(8, 8, 1)]

表示每个线程组有 8 × 8 × 1 = 64 个线程。

C# 侧调用 Dispatch 时传入的 x、y、z 决定一共有多少个线程组。比如:

[numthreads(8, 8, 1)]
Dispatch(kernel, 64, 64, 1);

表示一共会开启 (64×8, 64×8, 1×1) = (512, 512, 1) = 512 × 512 × 1 = 262144 个线程。

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

总线程数 = numthreads(x, y, z) × Dispatch(groupsX, groupsY, groupsZ)
         = (groupsX × x, groupsY × y, groupsZ × z)
         = groupsX × x × groupsY × y × groupsZ × z

注意:

  • 每个轴的线程或线程组数不能设置为 0,最小为 1
  • 1D 任务(一维数据,比如处理线性数组计算):x, 1, 1
  • 2D 任务(二维数据,比如处理图像、纹理等数据):x, y, 1
  • 3D 任务(三维数据,比如处理体积型数据,3D 纹理、网格等数据):x, y, z
  • 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)

Dispatch(groupsX, groupsY, groupsZ) 的规则

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

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

匹配规则

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

int groupsX = Mathf.CeilToInt(width / (float)numThreadX);
int groupsY = Mathf.CeilToInt(height / (float)numThreadY);
int groupsZ = Mathf.CeilToInt(depth / (float)numThreadZ);
  • groupsDispatch 传入的值
  • numThreadnumthreads 传入的值
  • widthheightdepth:代表你要处理的数据维度数量

CeilToInt 向上取整的目的是为了即使不是整数倍也能完全覆盖,超出部分的线程可以在 HLSL 中用边界判断过滤掉。

举例说明

比如要利用 Compute Shader 处理一个 512×512 像素的二维图像,numthreads[numthreads(8, 8, 1)],那么在 C# 侧:

int groupsX = Mathf.CeilToInt(512f / 8); // = 64
int groupsY = Mathf.CeilToInt(512f / 8); // = 64
Dispatch(kernel, groupsX, groupsY, 1);
// 实际启动的线程为 (64×8, 64×8, 1×1) = 512 × 512 = 262144
// 每一个线程处理一个像素,利用 GPU 并行性提升了效率

推荐规则

数据维度 推荐 numthreads 说明
1D 数据 (64,1,1)(128,1,1) 线性数组、粒子等
2D 数据 (8,8,1)(16,16,1) 图像、纹理等
3D 数据 (4,4,4)(8,8,8) 体积数据、3D 纹理等

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

总结

  • numthreads 定义线程组的大小(最多 1024 个线程)
  • Dispatch 定义要开启多少个组,一般按 数据尺寸 / numthreads 向上取整计算
  • 总线程数 = 它们的乘积
  • 所有维度都必须 >= 1
  • 1D:处理线性数据(数组、粒子)
  • 2D:处理平面数据(图像、屏幕)
  • 3D:处理体积数据(3D 纹理、网格)

100.2 知识点代码

Lesson100_性能优化_GPU_着色器优化_ComputeShader_组内线程数量.cs

using UnityEngine;

public class Lesson100_性能优化_GPU_着色器优化_ComputeShader_组内线程数量 : MonoBehaviour
{
    void Start()
    {
        #region 知识点一 ComputeShader 中的执行模型

        // Compute Shader 的执行模型可分为三层结构
        // 第一层 —— 线程
        //   HLSL 函数内部自动生成
        //   由 GPU 并行执行的最小计算单元
        //   负责执行入口函数的一次调用

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

        // 第三层 —— 线程组网格
        //   C# 侧 Dispatch() 调用
        //   定义多少个组要被 GPU 启动
        //   控制全局总线程数

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

        #endregion

        #region 知识点二 三个轴向的数字代表什么

        // 1. 入口函数前的 [numthreads(x, y, z)] 用于控制每个线程组一共有多少个线程
        //    比如:
        //    [numthreads(8, 8, 1)]
        //    表示每个线程组有 8 * 8 * 1 = 64 个线程
        // 2. C# 侧调用 Dispatch 时传入的 x、y、z 决定一共有多少个线程组
        //    比如:
        //    [numthreads(8, 8, 1)]
        //    Dispatch(kernel, 64, 64, 1);
        //    表示一共会开启 (64*8, 64*8, 1*1) = (512, 512, 1) = 512*512*1 = 262144 个线程

        // 也就是说它们共同决定了线程总数
        // 总线程数 = numthreads(x,y,z) * Dispatch(groupsX,groupsY,groupsZ)
        //          = (groupsX * x, groupsY * y, groupsZ * z)
        //          = groupsX * x * groupsY * y * groupsZ * z

        // 注意:
        // 每个轴的线程或线程组数不能设置为 0,最小为 1
        // 如果是 1D 任务(一维数据,比如处理线性数组计算):x,1,1
        // 如果是 2D 任务(二维数据,比如处理图像、纹理等数据):x,y,1
        // 如果是 3D 任务(三维数据,比如处理体积型数据,3D 纹理、网格等数据):x,y,z
        // x,y,z >= 1

        #endregion

        #region 知识点三 数值规则

        // 1. [numthreads(x, y, z)] 的规则
        //    三个参数都是常量整数
        //    取值范围:
        //    1-1. 每个维度 >= 1
        //    1-2. 每个维度 <= 1024
        //    1-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 数量),但通常非常大(几十万没问题)

        #endregion

        #region 知识点四 匹配规则

        // 当我们要处理二维纹理、数组、体积数据时
        // 应该让线程数覆盖整个数据区域
        // 一般情况下,线程数和分组数的公式如下
        // int groupsX = Mathf.CeilToInt(width / (float)numThreadX);
        // int groupsY = Mathf.CeilToInt(height / (float)numThreadY);
        // int groupsZ = Mathf.CeilToInt(depth / (float)numThreadZ);

        // groups:Dispatch 传入的值
        // numThread:numthreads 传入的值
        // width、height、depth:代表你要处理的数据维度数量

        // CeilToInt:向上取整的目的是为了即使不是整数倍也能完全覆盖
        //            超出部分的线程可以在 HLSL 中用边界判断过滤掉

        // 举例说明:
        // 比如我们要利用 ComputeShader 处理一个 512*512 像素的二维图像
        // numthreads 为:[numthreads(8, 8, 1)]
        // 那么在 C# 侧
        // int groupsX = Mathf.CeilToInt(512f / 8) = 64;
        // int groupsY = Mathf.CeilToInt(512f / 8) = 64;
        // Dispatch(kernel, groupsX, groupsY, 1);
        // 实际启动的线程为
        // (64*8, 64*8, 1*1) = 512 * 512 = 262144
        // 这样就每一个线程处理一个像素,利用 GPU 并行性提升了效率

        // 推荐规则
        // 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(NVIDIA) / Wavefront(AMD)(32 或 64 个线程)
        // 所以最好让 x*y*z 是 32 或 64 的倍数

        #endregion

        #region 总结

        // numthreads 和 Dispatch 中的 xyz 三个轴
        // numthreads 定义线程组的大小(最多 1024 个线程)
        // Dispatch 定义要开启多少个组,一般按数据尺寸 / numthreads 向上取整计算
        // 总线程数 = 它们的乘积
        // 所有维度都必须 >= 1
        // 1D:处理线性数据(数组、粒子)
        // 2D:处理平面数据(图像、屏幕)
        // 3D:处理体积数据(3D 纹理、网格)

        #endregion
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏