CHAPTER 37 / AMD · HIP 与 ROCm

HIP:从 CPU 数据到一次 kernel 调用

五个数怎样经历分配、搬运、执行与验证?

阅读与推演约 55 分钟练习时间另计

这一章要弄清楚

  • 区分 host/device 生命周期
  • 按 block/thread 检查索引
  • 区分提交、完成与正确

先备知识:引用与const:副本和同一个对象 / 真实资源与RAII:独占拥有 / Host/device、传输与异步生命周期 / Kernel 索引、边界与 grid-stride loop

C++20 本机逻辑模型;不是设备仿真或性能测量。厂商语法片段未在设备或 SDK 执行,实际 API 以匹配版本的公开文档为准。

先把调用拆成可检查的步骤

我们有五个整数 2、4、6、8、10,希望每个数加一。普通 C++ 循环会得到 3、5、7、9、11。写 HIP 时,数学问题没有变,改变的是工作安排:CPU 负责准备与协调,kernel 是让设备执行的函数,多个逻辑线程各处理不同索引。先理解这三件事,再记 API 名字,才能在结果错误时知道应检查哪一段。

Host 是主程序运行的 CPU 一侧,device 是被调用的计算设备。设备指针不是“另一个普通 vector”:分配方式、可访问位置和释放时机由相应 API 约束。某些系统支持统一地址或内存机制,但不能因此假定任意 host 指针都能交给任意 kernel。最初使用明确的 host 输入、device 输入输出、host 回读结果,数据关系更容易检查。

先画所有权,再写调用

最小流程包括选择设备、分配设备空间、复制输入、启动 kernel、等待结果、复制输出、验证和释放。错误处理贯穿每一步。分配失败后不能继续使用未成功取得的地址;某一步失败也不能遗忘此前成功分配的资源。RAII 依然有用,不过资源的生命必须覆盖所有异步使用,析构函数不会自动解决跨设备依赖。

例一用两个独立 vector 表达两个存储区。复制后修改 device_model 不会改变 host_input;只有明确回读才得到更新结果。它只验证数据流,不能模拟 HIP runtime、地址空间、GPU 并行执行或传输开销。把模型输出称为 GPU 实测,会混淆正在检查的命题。

多出来的线程也有合同

设 block 大小为四,五个元素需要两个 block,产生八个逻辑线程。全局索引等于 block 编号乘 block 大小,再加线程编号。索引零到四做有效工作,五到七必须跳过。向下取整只启动一个 block 会漏掉第五个数;不检查边界则会访问数组以外的存储。空输入最好由 host 明确处理,不依赖零大小 launch 的行为。

逐项读 kernel 和 launch(补充,另估25分钟)

先把普通 C++ 函数读法扩展到这一个 HIP 片段。__global__ 标记由 host 启动、在 device 上执行的 kernel;本例返回类型为 void,结果写入 outputblockIdx.x 是当前块的 x 编号,blockDim.x 是每块 x 方向的线程数,threadIdx.x 是当前线程在块内的 x 编号;编号从零开始。这里的点号仍是成员访问,三个名字由 HIP 提供。

dim3 表示三个无符号维度;dim3(4)(4, 1, 1),省略的 y、z 都是 1。下面注释中的 hipLaunchKernelGGL 按位置读取,不能把相邻的零随意理解成设备号:

位置 本例填写 作用
1 plus_one 要启动的 kernel
2 dim3((n+3)/4) grid:启动多少个块
3 dim3(4) block:每块多少个线程
4 0 每块额外动态共享内存的字节数,本例不申请
5 stream 已准备好的执行队列,工作提交到这个 stream
6 起 input, output, n 按 kernel 形参顺序传递的三个实参

**纸上走一次:**令 n=5。grid 为 (2,1,1),block 为 (4,1,1);块零覆盖索引 0–3,块一覆盖 4–7。i < n 让索引 4 写出 11,索引 5–7 跳过,最终五个数是 3、5、7、9、11。只把块大小改成 8 并同步修改 grid 的向上取整式,会得到一个块、仍有三个线程跳过。这里的加法式只用于小整数,不能忽略大 n 的溢出检查。

launch 配置没有表达“已经算完”。两个指针须指向该设备可访问且容量足够的存储,输入要先准备完成,输入、输出与相关资源还要存活到设备使用结束。下方是未执行的语法片段;上述数值是索引推演,CPU 示例不能证明 HIP 调用成功。读法核对自 HIP 7.1.52802 的 kernel 调用与 dim3,对应 docs-7.1.1,核对日期 2026-09-08。

// HIP 语法示意:未在 SDK/设备执行,不是完整 host 程序。
__global__ void plus_one(const int* input, int* output, int n) {
    const int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) output[i] = input[i] + 1;
}
// 已分配和准备 input/output;n > 0,且取值满足 int 范围。
// hipLaunchKernelGGL(plus_one, dim3((n+3)/4), dim3(4),
//                    0, stream, input, output, n);

这里使用很小的整数;真实输入还需检查长度转换、字节数乘法和加法是否溢出。四个线程只是索引算例,不是硬件配置建议。block 大小应结合 kernel 资源用量和设备限制选择,不能从示意图推导性能。

调用返回了,究竟证明什么

Host 发起工作后可能先继续执行。启动错误、异步执行错误和数值错误是三个层次:先检查 API 返回值及 launch 状态,再在规定同步点检查执行状态,最后与独立 reference 比较。打印输出指针只说明得到一串地址,不说明设备完成了写入。

本章固定一种输入类型、一个设备、一个显式 stream 和一次变换。后续再加入流水和 profiling。公开 HIP 文档还要匹配安装版本、目标架构和编译参数;本机例子不要求安装 SDK,也不证明任何目标设备支持情况。

观察 · 推演

五个数的存储迁移

host2 4 6 8 10device未分配01 / 03 · PIPELINEhost2 4 6 8 10device未分配01 / 03 · PIPELINE
准备

输出尚未产生。

1 / 3
阅读完整推演文字
  1. 准备

    host:2 4 6 8 10;device:未分配

    输出尚未产生。

  2. 执行

    host:2 4 6 8 10;model:3 5 7 9 11

    独立模型存储加一。

  3. 验证

    回读:3 5 7 9 11;oracle:3 5 7 9 11

    回读与oracle一致。

跟着例子,走完一遍

例题 01C++20 · 本机可运行

两个存储区与回读

[2,4,6,8,10],仅模型设备存储加一。

  1. 复制到独立vector
  2. 逐个加一
  3. 回读并检查原输入
33-a.cpp
下载
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <vector>
#include <stdexcept>

void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }

int main() {
 const std::vector<int> host{2,4,6,8,10};
 auto model=host; for(int& x:model) ++x;
 const auto result=model;
 require(host.front()==2 && result==std::vector<int>({3,5,7,9,11}));
 std::cout<<"host="<<host.front()<<"\nresult=";
 for(std::size_t i=0;i<result.size();++i) std::cout<<(i?" ":"")<<result[i];
 std::cout<<'\n';
}

如何编译和运行下载的 .cpp 文件 →

结果与解释

原输入首项2;结果3 5 7 9 11。

值复制建立不同对象。

在本机运行这个例子

下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。

clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 33-a.cpp -o example && ./example

预期标准输出:

host=2
result=3 5 7 9 11
例题 02C++20 · 本机可运行

八个索引处理五个数

n=5、block=4,统计每个元素访问次数。

  1. 向上取整得到两组
  2. 展开八个索引
  3. 仅i<n时访问
33-b.cpp
下载
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <vector>
#include <stdexcept>

void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }

int main() {
 constexpr std::size_t n=5, block=4;
 const auto blocks=n/block+(n%block!=0);
 std::vector<int> visits(n,0); std::size_t skipped=0;
 for(std::size_t b=0;b<blocks;++b) for(std::size_t t=0;t<block;++t) {
  const auto i=b*block+t; if(i<n) ++visits[i]; else ++skipped;
 }
 for(int count:visits) require(count==1);
 require(skipped==3);
 std::cout<<"blocks="<<blocks<<" valid="<<n<<" skipped="<<skipped<<'\n';
}
结果与解释

有效5,跳过3,每项访问一次。

边界保护保证覆盖,不提供性能结论。

在本机运行这个例子

下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。

clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 33-b.cpp -o example && ./example

预期标准输出:

blocks=2 valid=5 skipped=3
常见错误

把提交当完成

启动后立即读取或释放数据。

修正思路:建立完成依赖,检查执行错误,再回读验证和释放。

轮到你动手

先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。

练习 1

n=10、block=4,列出block 1和2的索引。

给我一点提示
  1. 编号从零开始
  2. 先算b*4再加线程号
查看答案与推理

block 1访问4–7;block 2访问8、9,跳过10、11。总共三组,不能省略最后一组。

练习 2

回读全零,设计三层检查。

给我一点提示
  1. 查执行
  2. 查字节和索引
  3. 查独立答案
查看答案与推理

检查API/launch及同步返回;核对初始化、元素数乘sizeof(T)、复制方向与输出指针;用n=1、5、8确定性数据对比oracle。每项定位不同故障,增加睡眠不能替代检查。

把理解说出来

先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。

解释

device pointer 能直接当 host vector 的 data() 吗?

参考回答 / English answer

先说明分配所在内存及哪侧允许访问;不能靠相同数值地址推定可访问。

A pointer value does not establish accessibility. I check the allocation type and access rules for each execution context.
预测

n=9、block=4,哪些线程跳过?

参考回答 / English answer

三组产生十二个索引;0–8有效,9–11跳过。

Three blocks produce twelve logical indices. Indices nine through eleven must not access the arrays.
调试

launch 没报错,结果不对,查什么?

参考回答 / English answer

先确认同步与执行错误,再查复制长度、索引、输入初始化和reference。

I separate launch validation from completion. Then I inspect transfers, indexing, initialization, and the reference.
追问

释放输入前为什么考虑 stream?

参考回答 / English answer

提交后设备可能尚未读取,资源必须覆盖所有未完成使用。

The device may still be reading after submission. Resource lifetime must cover every outstanding use.
解释

两个 vector 的模型证明什么?

参考回答 / English answer

只证明独立存储、变换与回读的C++逻辑,不证明设备通信或性能。

This is a CPU model of copying and ownership. It is neither a device simulator nor a GPU measurement.
调试

空输入与超大长度在哪里处理?

参考回答 / English answer

host处理空输入并检查类型、字节乘法和launch范围;kernel保护不能修复已溢出参数。

I validate sizes before allocation and launch. Kernel bounds checks cannot repair host-side overflow.

继续查证

公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。

接着看已有的图解

这些资料按主题补充本章内容。