CHAPTER 40 / AMD · HIP 与 ROCm
HIP Reduction:从正确性到 profiling 问题
算出21以后,怎样判断还值得优化哪一步?
这一章要弄清楚
- 处理非整块输入与空输入
- 分开kernel时间与端到端时间
- 用假设指导trace与counter分析
先备知识:可信测量、原始样本与性能模型 / Reduction、scan 与数值正确性 / HIP:从 CPU 数据到一次 kernel 调用 / Streams 与 events:用依赖组织异步工作
C++20 本机逻辑模型;不是设备仿真或性能测量。厂商语法片段未在设备或 SDK 执行,实际 API 以匹配版本的公开文档为准。
Reduction 的答案小,过程却有很多边界
Reduction把许多输入结合成一个结果,例如1到6求和得到21。并行做法可以先让每组求局部和,再把局部和相加。这样计算图改变了:原本的一条长链变成多条短链和一次合并。整数没有溢出时容易比较精确结果;浮点加法的结合顺序改变后,低位结果可能变化,需要独立制定误差合同。
例一把六个数分成容量四的两组,第一组得到10,第二组只有5、6,得到11,再合并为21。最后一组不足四个元素时不能随意读取两个额外位置。对于求和,可以把无效位置视为加法单位元零;最大值等其他操作则需要相应单位元及类型边界,不能一律补零。
Block 内的树不等于全局同步
HIP中常见做法让线程先累积局部结果,再在block内通过共享存储或适合的跨lane原语合并。若使用共享存储,读写阶段之间需要正确同步。某些线程因越界直接return,而其他线程进入要求全组参与的barrier,会破坏同步合同。更稳妥的初学设计是无效线程贡献单位元,仍参加必要同步。
多个block不能靠普通block barrier完成全局同步。可以先把部分结果写出,随后启动下一轮kernel,或使用适合问题与设备的库及协议。独立组的执行顺序不应假定为编号递增。本章的C++分组循环只是算法分解模型,并没有验证任何HIP设备同步代码。
从“快了”变成具体问题
假设一个程序的输入复制、kernel、输出复制分别用四、二、一单位时间,端到端总长七。把kernel优化到一单位,端到端变成六,而不是三点五。例二用这种明确给定的假设说明优化上限。真实计时还可能包含初始化、分配、提交、等待和额外验证,必须先给测量区间命名。
因此先收集trace:哪段时间最长,是否反复分配,是否每个小任务都回读,是否存在空闲间隙。随后才用counter研究具体kernel的访存、指令或资源情况。Counter名字与可用性依设备及版本而异,不要把某张卡的计数器列表当所有AMD产品的共同接口。
# 公开rocprofv3用法示意;本章未在GPU或SDK环境执行。
rocprofv3 --hip-trace --kernel-trace --memory-copy-trace -- ./reduction
命令展示要收集哪类活动,不承诺这台电脑已经安装工具或具备兼容设备。运行前应查匹配版本的帮助和硬件支持。Trace本身也可能带来额外开销;详细profiling运行与正式benchmark可以分别保存,避免把诊断成本混入最终性能结论。
一个优化只回答一个假设
例如假设小规模输入受提交成本限制,可以比较批处理与逐次调用;假设大规模受数据移动限制,可以检查字节量及有效带宽;假设block资源过多降低并发,则检查对应资源报告。任何假设都先保留正确baseline,再只改变相关因素,并重新运行边界、随机和数值测试。
最终报告应说明输入规模、类型、算法版本、构建flags、设备、软件版本、预热、重复样本和测量范围。CPU逻辑模型通过只证明算法例子;没有真实GPU运行不能通过GPU correctness或measurement gate。学习profiling最先要练会提出可证伪问题,而不是编造一张漂亮的speedup表。
独立局部和共同汇入一个结果
两个子组互不依赖,都将贡献给上方的总和。尾组不足四项。
partial0=10,partial1=11;没有 partial0→partial1 的依赖。
10+11=21,与顺序 oracle 21 相同。
阅读完整推演文字
- 分成两个独立组
sum:等待两组;group0:1 2 3 4;group1:5 6
两个子组互不依赖,都将贡献给上方的总和。尾组不足四项。
- 各自产生局部和
sum:等待合并;partial0:10;partial1:11
partial0=10,partial1=11;没有 partial0→partial1 的依赖。
- 合并并验证
sum:21 / oracle 21;partial0:10;partial1:11
10+11=21,与顺序 oracle 21 相同。
跟着例子,走完一遍
非整块输入的两级求和
[1,2,3,4,5,6],每组最多4项。
- 第一组和10
- 尾组和11
- 合并21并与顺序oracle比较
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
int main() {
const std::vector<int> a{1,2,3,4,5,6}; std::vector<int> partial;
for(std::size_t start=0;start<a.size();start+=4) {
int sum=0; for(std::size_t i=start;i<std::min(start+4,a.size());++i) sum+=a[i];
partial.push_back(sum);
}
const int result=std::accumulate(partial.begin(),partial.end(),0);
require(result==std::accumulate(a.begin(),a.end(),0) && partial==std::vector<int>({10,11}));
std::cout<<"partials="<<partial[0]<<' '<<partial[1]<<" sum="<<result<<'\n';
}
partials=10 11,sum=21。
尾部边界受min限制,没有额外读取。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 36-a.cpp -o example && ./example预期标准输出:
partials=10 11 sum=21
只优化kernel的端到端收益
传入4、kernel2、传出1;kernel改为1。
- baseline总长7
- 优化后总长6
- 区分kernel2倍与全流程7/6
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
int main() {
const int upload=4, download=1, old_kernel=2, new_kernel=1;
const int before=upload+old_kernel+download, after=upload+new_kernel+download;
require(before==7 && after==6);
std::cout<<"before="<<before<<" after="<<after<<" saved="<<(before-after)<<'\n';
}
before=7 after=6;仅节省1单位。
时间是给定算例,不是真实profiling输出。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 36-b.cpp -o example && ./example预期标准输出:
before=7 after=6 saved=1
用kernel时间冒充端到端
只减小kernel耗时,却宣称整个应用同倍加速。
修正思路:分别报告kernel与固定范围端到端,并解释其他成本。
轮到你动手
先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。
练习 1
十项1按组4求和,列partials与总和。
给我一点提示
- 尾组只有两项
- 对比顺序oracle
查看答案与推理
局部结果4、4、2,总和10;最后两项不能因不足整组被忽略。补单位元仅改变内部表示,不改变有效输入数。
练习 2
给一个值得profile的假设和反证。
给我一点提示
- 锁定规模
- 一次只改一个因素
查看答案与推理
假设小输入主要受逐次launch成本限制:保持总元素与算法固定,比较单次批处理和多次小launch,并用trace观察提交与空闲间隙。若kernel占绝大多数时间且批处理无改善,就不支持该假设,不能只报告最快样本。
把理解说出来
先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。
非整块输入为何不能让所有越界线程直接return?
参考回答 / English answer
若后续barrier要求全组参与,提前return可能破坏合同;应让无效线程贡献单位元并按规定同步。
Early return can violate a block-wide synchronization contract. Inactive elements can contribute the identity while threads still participate correctly.输入全负,求max能补零吗?
参考回答 / English answer
不行。零可能大于所有有效值;应选择对应类型的合适单位元并处理空输入。
Zero is not a valid maximum identity for arbitrary negative inputs. I define the identity and empty-input contract explicitly.局部求和相同,浮点合并结果为何不同?
参考回答 / English answer
结合顺序与舍入不同,需数值误差合同;不等同于数据race。
Floating-point addition is not associative. I distinguish rounding differences from synchronization bugs.block barrier可合并所有block吗?
参考回答 / English answer
普通block barrier范围仅当前block;跨block需下一轮kernel或合法全局协议。
A block barrier only coordinates that block. Global reduction requires another stage or a valid device-wide protocol.kernel快两倍,端到端为何只从7到6?
参考回答 / English answer
其他五单位成本未变;kernel只占baseline的2/7,优化受占比限制。
Only the kernel portion improved. The unchanged transfer costs limit the end-to-end gain.trace和counter分别先回答什么?
参考回答 / English answer
trace回答工作何时发生、在哪里等待;counter帮助解释kernel内部活动,两者不能互换。
A trace shows the execution timeline. Counters provide additional evidence about activity inside a kernel.继续查证
- HIP Reduction tutorial ↗
Reduction stages and optimization
- ROCprofiler quick guide ↗
Tracing options and hardware counters
公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。
接着看已有的图解
- ROCm 计算与 kernel 主题库 ↗
按内存、stream 和性能主题补充阅读;设备练习需满足该版本环境要求。
- GPU 调试方法主题库 ↗
按错误、卡住、性能回归分类选读;寄存器和工具命令须对照实际设备版本。
这些资料按主题补充本章内容。