CHAPTER 38 / AMD · HIP 与 ROCm
Streams 与 events:用依赖组织异步工作
两个 stream 怎样安全共享中间结果?
这一章要弄清楚
- 区分提交顺序和完成依赖
- 画跨stream生产消费边
- 解释延迟、吞吐及计时范围
先备知识:条件变量、有界队列与关闭协议 / 原子操作与 happens-before / Host/device、传输与异步生命周期 / HIP:从 CPU 数据到一次 kernel 调用
C++20 本机逻辑模型;不是设备仿真或性能测量。厂商语法片段未在设备或 SDK 执行,实际 API 以匹配版本的公开文档为准。
Stream 是有序工作序列
把复制输入、计算、复制输出连续写在 host 程序里,不代表三步已在设备完成。异步接口允许函数先返回,实际工作随后推进。Stream 表达有序命令序列;同一 stream 中的工作按接口规定的顺序执行。它不是一个固定 GPU 核,也不意味着每个 stream 独占一套硬件。
如果计算读取复制产生的数据,两者之间存在依赖。这条链无论放进多少 stream 都不能被数学上消除。增加 stream 给没有依赖的工作留下重叠机会,是否重叠还取决于复制条件、设备能力、kernel 资源占用和调度。用了两个 stream,并不自动得到两倍吞吐。
跨序列必须画出一条边
设 S0 复制输入后运行 producer,S1 运行 consumer。仅在 host 上先提交 producer、再提交 consumer,不能代替独立 stream 之间的执行依赖。可以在 producer 后记录 event,让 S1 等待此 event,再提交 consumer。Event 标记某个命令位置的进展,不是自动保护所有内存的 mutex。
// HIP API顺序示意,未在SDK/设备运行。
// producer 已在 s0 排队;各资源已创建,各调用须检查错误。
hipEventRecord(produced, s0);
hipStreamWaitEvent(s1, produced, 0);
// 随后把 consumer 提交到 s1。
记录必须发生在 producer 之后。把 event 放在 producer 前面,会得到形式上存在、语义上错误的依赖:消费者等到了某个事件,但所需数据还没有产生。调试时要检查 event 所代表的具体工作位置,而不只是搜索程序里有没有 wait。
用事件图理解,不用睡眠猜测
例一设上传结束于逻辑时刻二,producer耗时三,consumer耗时一。即使S1从时刻零空闲,consumer仍应从五开始,在六结束。这些数字是教学单位,不是毫秒或设备周期。模型计算依赖约束,不能模拟真实调度。
例二有两批独立数据,每批复制两单位、计算三单位。完全串行需要十;若明确假设复制和计算能使用独立资源流水,第二次复制可在第一次计算时进行,总长八。差异来自资源与依赖假设,真实设备是否符合必须看trace,不能把模型数字套到GPU上。
Buffer 的生命随异步工作延长
Host buffer交给异步复制后马上改写,可能破坏尚未读取的数据。输出地址交给下一次工作,也需确认上一轮消费者结束。双缓冲不是仅分配两块数组:每块数组都需经历可写、生产中、可读、消费中、可复用。复用条件由事件或同步表达,不能靠固定睡眠保证。
异步host/device复制要实现预期重叠,通常还需符合API要求的host memory条件。普通分页内存可能引入额外处理或同步,应查对应版本文档。初学时先用明确同步确保正确,再缩小同步范围寻找重叠,比一开始删除所有等待更容易定位问题。
先给计时范围命名
CPU只围住异步launch,常测到提交开销。围住提交加完成等待,才可能覆盖端到端时间;event计时则要明确两个标记在哪条时间线上,是否包含依赖等待。优化前后应固定输入、预热、资源、次数和范围。回答“多stream为什么没加速”时,先查依赖和瓶颈,再讨论更多并发。
动手改变 · 观察因果
把两批复制与计算放在同一时间轴
第二批复制结束以后,为什么计算还要等?
复制与计算各有独立资源,假设时长,无 D2H 阶段。两个缓冲区的默认结果为串行 10、流水 8;不是设备测量。 对应 例题 34-b;图中的代码行是步骤提示,完整可编译源码见例题。
改输入后从第一步重新推演。Tab 选择控件,Enter/空格操作按钮;图内方向键平移,手机可横向滑动。
先预测,再前进一步
第二批复制结束以后,为什么计算还要等?
输入、边界、状态变化完整文字推演与当前数据
静态推演与完整文字(便于对照、打印)
跨 stream 消费边:动作位置保持固定
上传完成后producer才可开始;consumer依赖尚未满足。
producer先完成写入,记录在它之后的event于5完成。
consumer等待event后从5开始,6结束。直到最后一个使用者完成,该缓冲区才可复用。
阅读完整推演文字
- 上传 0–2
upload:0–2;producer:等待上传;event:尚未完成;consumer:等待 event
上传完成后producer才可开始;consumer依赖尚未满足。
- 生产 2–5
upload:完成于 2;producer:2–5;event:于 5 完成;consumer:等待 event
producer先完成写入,记录在它之后的event于5完成。
- 消费 5–6
upload:完成于 2;producer:完成于 5;event:已满足;consumer:5–6
consumer等待event后从5开始,6结束。直到最后一个使用者完成,该缓冲区才可复用。
跟着例子,走完一遍
消费者等待生产者
上传2单位,producer3单位,consumer1单位。
- 上传0–2
- producer2–5
- consumer等待至5
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <algorithm>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
int main() {
const int producer_end=2+3;
const int consumer_end=std::max(0,producer_end)+1;
require(consumer_end==6);
std::cout<<"producer_end="<<producer_end<<" consumer_end="<<consumer_end<<'\n';
}
producer结束5,consumer结束6。
前驱约束决定最早开始时间。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 34-a.cpp -o example && ./example预期标准输出:
producer_end=5 consumer_end=6
两批数据逻辑流水
每批复制2、计算3,假设两类资源可重叠。
- 串行计算2+3+2+3
- 第二次复制在2–4
- 第二次计算等第一次在5结束
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <algorithm>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
int main() {
const int first_compute=2+3, second_copy=2+2;
const int pipelined=std::max(first_compute,second_copy)+3;
const int serial=2*(2+3);
require(serial==10 && pipelined==8);
std::cout<<"serial="<<serial<<" pipelined="<<pipelined<<'\n';
}
串行10,流水8,均为假设单位。
这是排程模型,不是硬件加速。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 34-b.cpp -o example && ./example预期标准输出:
serial=10 pipelined=8
用sleep替代依赖
睡一毫秒后假定数据准备好。
修正思路:以event或明确同步表达完成,负载变化仍正确。
轮到你动手
先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。
练习 1
A耗时4,B耗时2依赖A,C耗时3依赖B,何时完成?
给我一点提示
- 画A→B→C
- 空闲不代表可提前
查看答案与推理
A在4完成,B在6完成,C在9完成;放在不同stream不改变这条依赖链。
练习 2
设计异步输入buffer重用状态表。
给我一点提示
- 区分提交与完成
- 每次状态转移给出条件
查看答案与推理
可写→填充→复制中→复制完成→可重写。若同一块还承载输出,需加设备写入、回读和host消费完成条件。由event/同步驱动,不以睡眠驱动。
把理解说出来
先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。
两个stream意味着两个固定执行单元吗?
参考回答 / English answer
不。stream是顺序抽象,硬件资源和调度决定并行。
A stream is an ordered command sequence. It does not reserve a dedicated compute unit.event在producer前记录,等待它够吗?
参考回答 / English answer
不够。它只涵盖此前工作,必须标记所需生产工作完成。
The event must follow the producer. An earlier event does not establish the required dependency.CPU launch很短说明kernel很快吗?
参考回答 / English answer
不说明。可能只测提交,需明确完成边界或设备计时。
A short launch may only measure submission. I need a completion boundary or suitable device timing.双缓冲为何仍可能出错?
参考回答 / English answer
旧消费者未结束就再次写入同一buffer;每块buffer须有复用依赖。
Two buffers do not remove lifetime constraints. Each buffer needs a completion condition before reuse.多stream没有重叠,先看什么?
参考回答 / English answer
依赖图、trace、内存条件和资源瓶颈,而非直接增加stream。
I inspect dependencies and a trace first. Transfer conditions and resource limits may prevent overlap.stream wait和host等待的区别?
参考回答 / English answer
前者给设备序列加边,host可能继续;后者等待host可观察的完成。
A stream wait orders device work without necessarily blocking the host. Host synchronization waits for completion visible to the caller.继续查证
- HIP asynchronous execution ↗
Streams and events
- HIP host memory ↗
Pinned memory and asynchronous copies
公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。
接着看已有的图解
- ROCm 计算与 kernel 主题库 ↗
按内存、stream 和性能主题补充阅读;设备练习需满足该版本环境要求。
这些资料按主题补充本章内容。