HelloGPU-P02-经典算子与Kernel实战
第2篇 入门与硬件速通
第 8 章 Element-Wise:逐元素算子
本章做的所有事情可以用一句话描述:采用HIP或Triton程序实现,基于基线情况探索优化并发性能(主要是“提升有效带宽”)。可以从内存访存排布、合并事务与总线利用率,以及控制核函数启动、线程分工与循环展开等结构性开销等方面入手。
HIP或Triton程序是实现AMD GPU编程的两个不同途径,即同一目的的不同实现途径。所以除了从上述方面入手优化并发性能外,还可以比较这两种实现途径之间的性能差异。
8.1 逐元素算子的数据依赖
首先要分析清楚并发编程中,不同线程之间的输入、输出的依赖性情况,如果是相互独立是比较容易实现的。
在深度学习和矩阵计算中,逐元素算子(英文:Element-wise Operator,有时也称逐像素算子)是指:对输入张量(Tensor)中的每一个独立元素执行相同的标量级运算,且运算前后张量的形状(Shape)保持完全不变。
比如两个数组对应元素相加,结果保存到另一个数组中对应位置的示例就是逐元素算子。现代深度学习中大量的核心算子都属于这种逐元素算子(Element-Wise Operators):
| 算子 | 单个输出的表达式 | Y[i] 依赖什么 |
|---|---|---|
| Add(加法) | Y[i] = A[i] + B[i] |
仅依赖同索引的 A[i] 与 B[i] |
| ReLU(激活) | Y[i] = max(X[i], 0) |
仅依赖同索引的 X[i] |
| Scale & Bias(缩放平移) | Y[i] = alpha * X[i] + beta |
仅依赖 X[i] 与全局常量标量 |
| Clamp(截断) | Y[i] = min(max(X[i], low), high) |
仅依赖 X[i] 与常量上下界 |
这些算子的共同点是:计算一个输出不需要其他输出的结果。程序可以通过索引公式与启动配置安排数据分工,再由硬件调度执行。同样的数学公式可以采用不同分工,后面的实验会比较这些选择对时间的影响。
与逐元素算子相对的,还有一个概念叫归约算子,这是下一个章节要介绍的内容。
8.2 固定数学语义与正确性标准
在做基线实现与优化尝试与测试前,我们首先要明确算子中核心数学逻辑、规范与必要限制。比如:
| 项目 | 固定方式 |
|---|---|
| 数学语义 | C[i] = A[i] + B[i] |
| 数据类型 | 32 位浮点数(FP32) |
| 输入 | 用同一确定性整数公式生成,再转成 FP32 |
| 参考结果 | CPU 逐元素执行同一个 FP32 加法 |
| 边界 | 覆盖小于 wavefront、刚好等于 block、比 block 多 1、不能被 4 整除等长度 |
| 正确性 | 正式计时前检查一次,计时后再检查一次 |
| 计时范围 | GPU event 包围一次 kernel 提交,不含分配、输入生成与 Host-to-Device 拷贝 |
在实现算子过程中,一定要注意边界检查。
8.3 建立成本模型和瓶颈假设
基于数组相加示例,给出了并行编程中经常用到的两个指标:
逻辑算术强度,是指算法单位逻辑读写量上执行的逻辑浮点运算次数: \[ AI_{logical} = \frac{1\ \text{FLOP}}{12\ \text{Byte}} \approx 0.0833\ \text{FLOP/Byte} \]
逻辑有效带宽,是指“单位kernel 提交的 GPU event 时间”下执行的逻辑数据读写量: \[ BW_{effective} = \frac{3 \times N \times 4\ \text{Byte}}{t} \]
后者是用来评测并发程序的性能效果的一个常用指标。我们在后续会不断用到。
8.4 实现向量加法
在给出了此次并发程序的输入输出数据的规范、输出检查、计时统计口径与具体实施规范、多次测试数据的取舍与综合规范、输出结果与日志规范等程序实现与评测的上层规范后,这个章节具体做了如下几个事情:
- 基线v0:一线程一元素写法,相邻线程访问相邻元素。
- 优化尝试v1:一个线程处理多个元素(分多轮来处理),改变同轮访问顺序(其中给出两个具体实现思路:连续排列时多个相邻线程访问相邻元素、跨步排列时相邻线程访问的元素存在指定逻辑间隔)。
- 优化尝试v2:减少 block 数,一个线程处理多个元素(分多轮来处理),单个线程跨着读,同一轮的相邻线程仍然连续读。
- 优化尝试v3:一次表达四个元素。具体实现见
float4:一次表达四个元素是否更快 。
8.5 按问题选择性能分析
指出每次优化尝试与测试时必做的事情、根据当前步得到的结果与想要理清的问题再决定下一步尝试优化的方向。
并给出了时间线与工具适用范围。
8.6 HIP 与 Triton 对照
总结了两个实现途径下多次优化尝试时得出的结果,将其汇总在一张图中如下(HIP v0是基线):
在实现途径方面(即使用HIP还是Triton):基于当前严谨的统计数据,我们不能得出“某种语言在性能上压倒另一种”的轻率结论。
采用HIP实现时,根据统计数据,不能从v1/v2/v3的优化尝试结果数据中得出某次明确稳定的性能提升。
本章小结
- 输出独立,分工可以改变:一个输出只依赖对应输入,可以比较不同的线程分工与数据块大小。低算术强度提示优先检查数据访问,但不能据此断定显存带宽是唯一瓶颈。
- 两条路线各有控制方式:HIP 从线程索引出发,比较地址排列、grid-stride 循环与四元素分组;Triton 从分块张量出发,比较 tile 大小。
- 每轮都检查并复测:先核对结果,再按相同口径测量,与基线及上一轮比较。独立复测与样本范围帮助判断收益是否稳定,没有提速的结果也要保留。
- 有具体问题时再做 profiling:正确性与性能比较可以支持保留或回退的决定;需要进一步解释时,再选择能够提供相关证据的工具。