CHAPTER 33 / 架构、性能与并行
Host/device、传输与异步生命周期
launch 返回后能否立即读结果,为什么数据留在设备上可能比优化一条指令更有效?
这一章要弄清楚
- 画出准备、传输、计算、回传的依赖
- 用完成事件约束缓冲区生命周期
- 区分可重叠条件与理论流水线模型
先备知识:真实资源与RAII:独占拥有 / 原子操作与 happens-before / Reduction、scan 与数值正确性
C++20 / macOS 与 Linux;本章的硬件模型只推演逻辑,不代表设备性能。
两个执行环境,一份计算目标
Host 通常是负责组织任务的 CPU 端,device 是执行加速计算的设备。对离散 GPU,host 内存和 device 内存一般有明确的访问与传输规则;某些平台提供统一地址空间、托管内存或共享物理内存,但不能因此假设任何指针在任意端都可解引用。指针值、地址可达性、数据驻留位置和访问完成时间,是四个不同问题。
从输入 1、2、3 算出两倍值,最清晰的逻辑路径是准备 host 输入、建立设备存储、H2D 传入、kernel 计算、D2H 回传、CPU 验证、释放资源。第一例用两个普通 vector 代表两端,用显式状态检查说明依赖。它实际只运行 CPU 代码,没有实现 GPU runtime、DMA 或设备模拟器;模型通过只表示这一流程逻辑自洽。
提交完成不是执行完成
异步 launch 往往表示命令已提交,host 可以继续做其他工作,不代表 kernel 已结束。要读取结果或重用存储,需要等待能覆盖相关操作的完成事件。错误也有两个时间点:提交时可以发现配置等问题,执行时才可能发现非法访问。只检查 launch 返回值而不观察完成,容易把“成功入队”误当作“正确计算”。
Host 到 device 的异步拷贝也有生命周期合同:如果 API 仍可能读取源缓冲,调用者不能提前修改、释放或使其地址失效。设备输出未完成回传前,host 不能读取未完成的目标。用 RAII 包装资源有帮助,但异步任务可能跨越局部作用域,因此析构策略必须等待、延迟释放或由更长寿命的所有者管理;单纯在函数末尾 free 不够。
流与事件建立局部顺序
一个 stream 可理解为有顺序关系的命令队列。同一流中相关操作按 API 合同建立顺序;不同流之间不能靠源代码先后自动推断依赖,通常需要事件或其他显式同步。默认流行为还有具体平台规则,初学时使用显式流更容易审查。全设备同步很容易获得正确结果,却可能无谓阻塞无关工作;逐步验证后再缩小等待范围。
尽量把中间结果留在 device 上。若一个流水线依次做归一化、矩阵乘、归约,每一步都回 CPU 再传回设备,会增加固定延迟与字节量。端到端测量要包括这些传输,不能只挑最快 kernel。一份峰值算力参数并不说明小任务能加速,启动和搬运可能支配总时间。
流水线的收益带有前提
第二例设 H2D=2、计算=5、D2H=2 个抽象时间单位,三个块串行需要27。若三类阶段能独立执行、使用两组缓冲,并按完成依赖复用,模型完工时间为19。这里的数字是手算参数,不是毫秒,也不是任何设备的实测。真实重叠还依赖 copy engine、互联、内存带宽、数据依赖和 host 内存类型,不能只创建两个 stream 就保证实现。
Pinned host memory、双缓冲和较大批次可能有助于异步传输,但锁页内存也是有限资源。选块大小需要权衡固定开销、可重叠程度和内存占用。面试时画出每个 buffer 从准备到可复用的时间线,标明哪条事件保护它,比笼统说“异步更快”更有说服力。
动手改变 · 观察因果
传输、计算和缓冲区何时可以重叠
下一阶段等的是数据、执行资源,还是缓冲区?
每类阶段占一条独立资源,按批次 FIFO,无启动开销。时长全为假设单位,不是真实设备性能;双缓冲默认复现 27 → 19。 对应 例题 29-b;图中的代码行是步骤提示,完整可编译源码见例题。
改输入后从第一步重新推演。Tab 选择控件,Enter/空格操作按钮;图内方向键平移,手机可横向滑动。
先预测,再前进一步
下一阶段等的是数据、执行资源,还是缓冲区?
输入、边界、状态变化完整文字推演与当前数据
静态推演与完整文字(便于对照、打印)
输入与输出的可用时刻
host输入存在,逻辑设备存储还未上传。
device有自己的输入副本,随后才能计算。
device先算成2、4、6,回传完成后host结果可读。
阅读完整推演文字
- 准备输入
host:1 2 3;device:空;输出:不可读
host输入存在,逻辑设备存储还未上传。
- 完成上传
host:1 2 3;device:1 2 3;状态:uploaded
device有自己的输入副本,随后才能计算。
- 计算与回传完成
device:2 4 6;host结果:2 4 6;状态:complete
device先算成2、4、6,回传完成后host结果可读。
跟着例子,走完一遍
显式执行状态防止过早读结果
host=[1,2,3],CPU模型的设备任务为每元素乘二。
- 未上传时运行被拒绝。
- 上传复制到单独存储,运行完成后才能回传。
- 验证结果[2,4,6],并确认原输入未改变。
#include <iostream>
#include <stdexcept>
#include <vector>
class DeviceModel {
enum class State {empty,uploaded,complete};State state=State::empty;
std::vector<int> memory;
public:
void upload(const std::vector<int>& input) {memory=input;state=State::uploaded;}
void run() {
if(state!=State::uploaded) throw std::logic_error("input not ready");
for(auto& x:memory) x*=2;state=State::complete;
}
std::vector<int> download() const {
if(state!=State::complete) throw std::logic_error("output not ready");return memory;
}
};
int main() {
DeviceModel device;bool rejected=false;
try {
const auto unexpected=device.download();
std::cout<<"unexpected early result size="<<unexpected.size()<<'\n';
return 1;
} catch(const std::logic_error&){rejected=true;}
const std::vector<int> input{1,2,3};device.upload(input);device.run();const auto output=device.download();
if(!rejected || output!=std::vector<int>{2,4,6} || input!=std::vector<int>{1,2,3}) throw std::runtime_error("model");
device.upload({});device.run();if(!device.download().empty()) throw std::runtime_error("empty");
std::cout<<"result=2,4,6 early_read_rejected=1\n";
}result=2,4,6 early_read_rejected=1
两个vector只用于教学数据所有权;不模拟设备指令或性能。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 29-a.cpp -o example && ./example预期标准输出:
result=2,4,6 early_read_rejected=1
双缓冲流水线的依赖模型
三个块,三个独立阶段耗时2、5、2,缓冲区数量2。
- H2D必须等对应缓冲区前一次D2H完成。
- 计算等待本块H2D及前块计算。
- D2H等待本块计算及前次D2H。
#include <algorithm>
#include <array>
#include <iostream>
#include <stdexcept>
int main() {
std::array<int,2> buffer_free{0,0};int upload=0,compute=0,download=0;
std::array<int,3> finished{};
for(std::size_t chunk=0;chunk<3;++chunk) {
const auto slot=chunk%2;
upload=std::max(upload,buffer_free[slot])+2;
compute=std::max(compute,upload)+5;
download=std::max(download,compute)+2;
buffer_free[slot]=download;finished[chunk]=download;
}
if(finished!=std::array<int,3>{9,14,19}) throw std::runtime_error("dependency schedule");
std::cout<<"serial_units="<<3*(2+5+2)<<" pipeline_units="<<download<<'\n';
}serial_units=27 pipeline_units=19
这是理想化调度算式,无真实设备或延迟测量。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 29-b.cpp -o example && ./example预期标准输出:
serial_units=27 pipeline_units=19
异步调用返回就释放源缓冲
H2D尚未完成便释放host输入,或kernel尚在写时复用device输出。
修正思路:让缓冲区寿命覆盖所有未完成访问;用适当事件确认相关操作完成后再读取、修改或释放。
轮到你动手
先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。
练习 1
一个异步上传引用 vector.data(),调用后立刻 push_back 导致扩容。问题在哪里?
给我一点提示
- 上传什么时候不再读取源地址?
- vector扩容会使旧地址失效。
查看答案与推理
未完成上传可能访问已释放的旧存储,属于生命周期错误。上传完成前保持容量与内容稳定;或使用专门拥有且被完成事件保护的传输缓冲。
练习 2
理想三阶段改成2、2、2,三个块且足够缓冲,串行与完工时间各是多少?
给我一点提示
- 第一块需走完三阶段。
- 之后每2单位能完成一块。
查看答案与推理
串行为18,理想流水为首块6加后两块4,共10。结果需要独立阶段资源和正确依赖;共享带宽或无法重叠会使实际时间更大。
把理解说出来
先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。
host 与 device 指针数值一样,能直接互相解引用吗?
参考回答 / English answer
不能只凭数值判断,需要知道内存分配类型、平台可达性、驻留与同步合同。
A pointer value alone does not establish accessibility. The allocation type, platform, and synchronization contract determine valid access.launch 返回成功是否证明计算成功?
参考回答 / English answer
只代表提交阶段通过了相关检查,异步执行错误还要在同步或事件完成处观察,并验证输出。
Successful submission is not successful completion. I check completion errors and validate the produced data.不同 stream 的命令如何建立依赖?
参考回答 / English answer
使用明确事件或API提供的同步关系,不能只凭host源码顺序假设不同流互相等待。
I use explicit events or documented synchronization. Host submission order alone does not necessarily order different streams.为什么尽量保留中间结果在 device?
参考回答 / English answer
减少反复H2D/D2H及固定传输开销,尤其对多个连续kernel;同时需要管理设备内存容量。
Keeping intermediates on the device avoids repeated transfers. The tradeoff is device-memory capacity and lifetime management.本章流水线19个单位是不是某GPU的测量?
参考回答 / English answer
不是,来自假设独立阶段的调度模型,单位为抽象时间;真实重叠需要设备验证。
It is an analytical schedule with assumed durations. It is not a measured GPU result.为什么两个 stream 未必比一个更快?
参考回答 / English answer
依赖、copy engine、带宽、锁页内存和任务粒度可能限制重叠;必须端到端计时。
Streams expose concurrency but do not create extra bandwidth or engines. Dependencies and resource limits can eliminate the expected overlap.继续查证
- AMD HIP:Memory management ↗
Host/device memory 与分配管理
- AMD HIP:Asynchronous execution ↗
Streams、events、异步并发
公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。
接着看已有的图解
- GPU 执行层级与 wave 图解 ↗
辅助对照软件线程与硬件执行单位;旧调试题中的最后一段 wave 隐式同步不能作为正确实现,按本书第 31/36 章证明同步。
- HIP 基础:Grid、Block 与 Thread ↗
复习线程索引与 host/device 调用顺序,运行时与驱动的职责以本书第 35 章及修正版 Atlas 为准。
这些资料按主题补充本章内容。