CHAPTER 38 / AMD · HIP 与 ROCm

Streams 与 events:用依赖组织异步工作

两个 stream 怎样安全共享中间结果?

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

这一章要弄清楚

  • 区分提交顺序和完成依赖
  • 画跨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/空格操作按钮;图内方向键平移,手机可横向滑动。

正在准备默认算例。下方例题包含完整源码与逐步解释。

第 1 步

先预测,再前进一步

第二批复制结束以后,为什么计算还要等?

输入、边界、状态变化

完整文字推演与当前数据
    静态推演与完整文字(便于对照、打印)
    观察 · 推演

    跨 stream 消费边:动作位置保持固定

    upload0–2producer等待上传event尚未完成consumer等待 event01 / 03 · TIMELINEupload0–2producer等待上传event尚未完成consumer等待 event01 / 03 · TIMELINE
    上传 0–2

    上传完成后producer才可开始;consumer依赖尚未满足。

    1 / 3
    阅读完整推演文字
    1. 上传 0–2

      upload:0–2;producer:等待上传;event:尚未完成;consumer:等待 event

      上传完成后producer才可开始;consumer依赖尚未满足。

    2. 生产 2–5

      upload:完成于 2;producer:2–5;event:于 5 完成;consumer:等待 event

      producer先完成写入,记录在它之后的event于5完成。

    3. 消费 5–6

      upload:完成于 2;producer:完成于 5;event:已满足;consumer:5–6

      consumer等待event后从5开始,6结束。直到最后一个使用者完成,该缓冲区才可复用。

    跟着例子,走完一遍

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

    消费者等待生产者

    上传2单位,producer3单位,consumer1单位。

    1. 上传0–2
    2. producer2–5
    3. consumer等待至5
    34-a.cpp
    下载
    // 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';
    }
    

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

    结果与解释

    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
    
    例题 02C++20 · 本机可运行

    两批数据逻辑流水

    每批复制2、计算3,假设两类资源可重叠。

    1. 串行计算2+3+2+3
    2. 第二次复制在2–4
    3. 第二次计算等第一次在5结束
    34-b.cpp
    下载
    // 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,何时完成?

    给我一点提示
    1. 画A→B→C
    2. 空闲不代表可提前
    查看答案与推理

    A在4完成,B在6完成,C在9完成;放在不同stream不改变这条依赖链。

    练习 2

    设计异步输入buffer重用状态表。

    给我一点提示
    1. 区分提交与完成
    2. 每次状态转移给出条件
    查看答案与推理

    可写→填充→复制中→复制完成→可重写。若同一块还承载输出,需加设备写入、回读和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.

    继续查证

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

    接着看已有的图解

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