当前位置: 华文问答 > 数码

gpu编程需要怎么做才能最小化内存访问,最大化运算并行,用并行运算的吞吐量弥补访存延时方面的劣势?

2021-08-07数码

减少内存访问

一种思路是 减少访问的数据量

  • 优化算法本身。
  • 降低数据精度 (如FP16/INT8,不过要考虑硬件是否擅长这种数据类型的后续计算)。
  • 利用broadcast/shuffle 在wave/warp/subgroup间低开销共享数据 [1] [2] [3]
  • 利用 push constant 消除不必要的访存, 一些硬件支持kernel启动前预先装载少量数据到GRF 。不过这通常意味着数据不能是memory resource,而是state中的一些常量(比如HLSL里就是root sginature里的constant,OpenCL里是非指针的arg)。
  • 另一个思路是 减少昂贵的内存访问

  • 不同类型的资源有不同的延迟与带宽 ,比如sampler吞吐可能不会比untyped resource大(压缩的资源除外)但延迟却更高,texture还有格式排布的要求,一些format还可能因为硬件不支持而悄悄被驱动做转换。
  • 优化global访存到SLM ,不过数据共享不够多的话可能意义不大,因为很多时候 GPU自己会做cache ,而用SLM反而增加 同步开销
  • 同样是atomic,一些硬件做SLM-based比global-base要开销小 [4] 。更进一步,甚至可以先在wave内做reduce/scan再由一个线程进行atomic操作 [5]
  • 一些平台 对某些访问的延迟额外高 ,尽量避免。比如intel的gen11以前SLM并不快,gen12的typeduav很慢。
  • 优化数据排布 ,coalescence read能减少访存的浪费 [6] ,有规律的访问也能降低cache miss的惩罚。
  • 此外还有一些trick来降低访存延迟:

  • 一次性读取更多数据 可能能有更低的延迟。很多硬件一条指令能读取不同长度的数据,一次性读更大的数据就意味着更少的指令(以及更少的计算offset的开销)。哪怕数据带宽没改变,最终延迟上可能还是比多条指令低一些。
  • 一些硬件有独特的读取指令 ,同样大小的数据读出不同的layout,减少后续transpose/reshape的开销。比如intel的blockread读4个float跟普通read读float4是不同的意义 [7] ……
  • 增加运算并行

    对于单个线程,关键就是增加ILP:

  • 减少数据间依赖 (可以考虑展开循环)。
  • 有时候 寄存器压力也会降低ILP 。比如8个空闲GRF理论上可以issue8个read到队列,然后后续一并计算。但考虑到计算offset要占用GRF,后续计算的中间数据也要占用GRF,于是不得不只读一部分马上wait然后做运算……这个要根据asm去分析了。
  • 有时候 分支也会降低ILP 。还是比如读8个数据按需做计算,用上ifelse,可能 编译器会把每个数据和处理单独放进basic block ,这样 多个读取的延迟都不能被互相掩盖了 。这个也得看asm,而且编译器的心思不好猜所以不方便做优化。常见的做法有condition转化为and或乘法,或者读完再if替换成0,但都见过被编译器识破的,反而有时候把读取和计算分开两个循环,编译器就呆住不动了……而且对于ifelse在不diverge的时候反而是优点。
  • 对于多个线程,通常来说线程越多硬件越能发掘出并行的潜力:

  • 对于一些带循环的算法,可以 考虑把循环拆分 到多个threadgroup计算,最后再reduce,这在任务比较小没法充分利用硬件资源时更管用,但也要记住同步与SLM的开销。
  • 对于多个任务间的并行,还可以考虑交织访存运算为主的kernel来合理利用不同组件。

    优化是个深坑,是个无底洞。

    如果绕不开,不要心存侥幸,乖乖看asm上工具做分析,源代码、dxbc、dxil这种层面的东西很可能没用的……

    参考

    1. ^ warp shuffle https://developer.nvidia.com/blog/using-cuda-warp-level-primitives/
    2. ^ wave shuffle https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/wavereadlaneat
    3. ^ subgroup shuffle https://github.com/KhronosGroup/OpenCL-Docs/blob/master/ext/cl_khr_subgroup_extensions.asciidoc#general-purpose-shuffles
    4. ^ shared atomic https://developer.nvidia.com/blog/gpu-pro-tip-fast-histograms-using-shared-atomics-maxwell/
    5. ^ wave reduce atomic https://github.com/microsoft/DirectX-Specs/blob/master/d3d/HLSL_ShaderModel6_5.md#example-usage
    6. ^ coalescence read https://developer.nvidia.com/blog/how-access-global-memory-efficiently-cuda-c-kernels/
    7. ^ block read https://www.khronos.org/registry/OpenCL/extensions/intel/cl_intel_subgroups.html#_add_a_new_p_6_13_x_sub_group_read_and_write_functions