CHAPTER 35 / 架构、性能与并行
合并访问、共享内存与同步范围
为什么线程访问相邻地址很重要,shared memory 又为什么必须配合分阶段同步?
这一章要弄清楚
- 计算一组lane访问的地址区域
- 区分global、block共享区与寄存器
- 解释barrier的参与范围和尾部策略
先备知识:CPU cache、缓存一致性与 SIMD / Reduction、scan 与数值正确性 / Kernel 索引、边界与 grid-stride loop
C++20 / macOS 与 Linux;本章的硬件模型只推演逻辑,不代表设备性能。
看同一时刻的一组地址
GPU 上的相邻线程通常成组执行。讨论内存合并(coalescing)时,不是看某一个线程沿时间连续访问了什么,而是看一组线程在同一次访问指令上请求哪些地址。若连续lane读取连续四字节元素,地址更容易被少量内存事务服务;若每个lane相隔很远,就可能涉及更多区域。实际事务粒度、对齐要求和缓存行为取决于架构,不能把一种GPU的细节固定成所有公司的规则。
第一例选32个lane、4B元素、128B区域作为教学参数,统计区域覆盖。连续地址0..124只涉及一个区域;步长32个元素则地址每次增加128,涉及32个区域。它没有执行GPU访存,也没有模拟缓存、事务拆分或重放,因此输出叫region count。只有在设备上结合profiler才能讨论实际事务数、带宽效率与瓶颈。
shared memory 是显式管理的暂存区
许多GPU编程模型提供一个block内可共享的低延迟存储区域;AMD常见硬件术语是LDS。它与每线程的寄存器不同,也不是所有block都能随意访问的全局仓库。程序把会复用的数据加载到这个区域,块内线程再多次使用,有机会减少global访问。收益来自复用和访问布局,而不是变量名里出现shared就自动加速。
共享区通常有bank结构,不同lane访问某些地址模式可能出现bank冲突。具体映射、广播与冲突规则需要查目标架构。padding有时能改变冲突模式,但也增加共享存储占用;更多共享存储或寄存器可能降低同时驻留的block数量。因此优化需要同时看带宽、复用、占用资源与实际吞吐,不能只追求理论occupancy百分比。
barrier 连接生产与消费阶段
合作计算一般有“加载→同步→使用→同步→复用存储”的阶段。加载后同步确保消费者不读取另一个线程尚未写好的槽。消费后同步确保下一轮加载不会覆盖仍被别人使用的数据。block barrier通常只约束同一block的参与线程,不能让任意两个block形成全局完成点。跨block归约常通过后续kernel、适用的原子协议或特殊协作机制组织,不能假设block按编号运行。
第二例以四个逻辑lane归约三个输入3、1、4。第四个lane没有合法输入,写入加法单位元0;所有lane概念上经过加载完成点,再进行两轮合并。CPU程序用阶段快照说明谁依赖哪一轮值,但没有真正调用GPU barrier,所以它只是算法逻辑模型。真实kernel必须另外验证barrier位置与参与合同。
尾部线程不等于不存在
在有barrier的kernel中,不能简单让越界lane从函数返回,而让其余lane继续同步。更稳妥的结构是每个lane都有共享槽,无效输入加载单位元,按统一控制流到达必要的同步点;最后只让有效输出位置写回。复杂分支需对照具体API合同,避免把“条件大概一样”当成参与一致。
常见调试顺序是先验证小矩阵和非整除尺寸,再检查每轮共享区初始化、读写范围及barrier。把所有输出都用零初始化可能暂时遮住漏写,所以测试中可使用哨兵并核查全部位置。面试回答应能指出同步保证的范围、依赖的数据以及为什么下一阶段现在可以开始,而不是只在每行代码后补一个同步调用。
三输入装入四槽共享区
lane3没有输入,但仍写0并参与阶段。
读取上一阶段,lane0=3+4,lane1=1+0。
lane0=7+1=8;最终写回只需一个线程。
阅读完整推演文字
- 合作加载
lane0:3;lane1:1;lane2:4;lane3:0
lane3没有输入,但仍写0并参与阶段。
- 间隔二合并
lane0:7;lane1:1;阶段:加载完成后
读取上一阶段,lane0=3+4,lane1=1+0。
- 间隔一合并
lane0:8;输出:8;范围:单block模型
lane0=7+1=8;最终写回只需一个线程。
跟着例子,走完一遍
一组lane覆盖的区域数
教学模型:32lane、4B元素、128B区域;步长1与32。
- 计算lane×stride×4字节地址。
- 地址除以128得到区域号。
- 比较去重后的覆盖数,不声称是实际GPU事务。
#include <cstddef>
#include <iostream>
#include <set>
#include <stdexcept>
std::size_t regions(std::size_t stride) {
std::set<std::size_t> ids;
for(std::size_t lane=0;lane<32;++lane) ids.insert(lane*stride*4/128);
return ids.size();
}
int main() {
if(regions(1)!=1 || regions(32)!=32 || regions(2)!=2) throw std::runtime_error("address model");
std::cout<<"contiguous_regions="<<regions(1)<<" strided_regions="<<regions(32)<<'\n';
}contiguous_regions=1 strided_regions=32
同一指令中相邻lane的地址集中度影响潜在合并机会。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 31-a.cpp -o example && ./example预期标准输出:
contiguous_regions=1 strided_regions=32
无效lane写单位元后参加阶段归约
block逻辑宽度4,输入[3,1,4];第四槽置0。
- 加载全部四槽,尾部填0。
- 第一轮利用同一旧阶段快照合并间隔2的值。
- 第二轮再合并,检查0..4输入长度。
#include <array>
#include <cstddef>
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <vector>
int block_reduce(const std::vector<int>& input) {
if(input.size()>4) throw std::invalid_argument("single block model");
std::array<int,4> shared{};
for(std::size_t lane=0;lane<4;++lane) shared[lane]=lane<input.size()?input[lane]:0;
// Logical phase boundary, not a GPU barrier or a device simulation.
for(std::size_t stride=2;stride>0;stride/=2) {
const auto previous=shared;
for(std::size_t lane=0;lane<stride;++lane) shared[lane]=previous[lane]+previous[lane+stride];
}
return shared[0];
}
int main() {
for(std::size_t n=0;n<=4;++n) {
std::vector<int> input(n);std::iota(input.begin(),input.end(),1);
if(block_reduce(input)!=std::accumulate(input.begin(),input.end(),0)) throw std::runtime_error("tail");
}
bool rejected=false;
try {
const int unexpected=block_reduce({1,2,3,4,5});
std::cout<<"unexpected oversized block sum="<<unexpected<<'\n';
return 1;
} catch(const std::invalid_argument&){rejected=true;}
if(!rejected || block_reduce({3,1,4})!=8) throw std::runtime_error("block");
std::cout<<"block_sum=8 inactive_lane=0\n";
}block_sum=8 inactive_lane=0
CPU阶段模型表达数据依赖;未验证真实GPU同步实现。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 31-b.cpp -o example && ./example预期标准输出:
block_sum=8 inactive_lane=0
部分线程return,其他线程到达barrier
非整除尺寸中无效lane提前退出,共享区漏初始化,并可能违反block同步参与规则。
修正思路:用掩码加载和合适单位元保持必要参与;统一经过barrier,只对有效输出做写回。
轮到你动手
先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。
练习 1
在128B区域模型中,连续32个4B元素的首地址从0改成4,涉及几区?
给我一点提示
- 最后元素起点为128。
- 统计区域0与1。
查看答案与推理
前31个元素在区域0,最后一项从128开始在区域1,共2区。这只表示地址范围被拆开,实际事务仍依赖设备。
练习 2
分块矩阵乘法每轮加载新tile之前为什么常还需要一个barrier?
给我一点提示
- 上一轮最后一个消费者可能比你慢。
- 共享槽位会被下一轮复用。
查看答案与推理
加载前的阶段边界防止某lane覆盖另一lane仍在读取的旧tile。加载后的barrier保护新数据就绪,两者分别保护不同依赖,不能只保留其中一个。
把理解说出来
先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。
合并访问看的是单线程连续性还是一组线程地址?
参考回答 / English answer
主要看同一次访存指令上相关lane的地址分布,单线程沿时间连续并不保证组内集中。
Coalescing concerns the addresses requested together by a group of lanes. One thread’s sequential history is not enough.shared memory为什么可能让kernel更慢?
参考回答 / English answer
加载和barrier有成本,复用不足难以回本;bank冲突或资源占用降低并行驻留也会拖慢。
Shared memory adds loading and synchronization cost. Limited reuse, bank conflicts, or resource pressure can outweigh the benefit.block barrier能同步其他block吗?
参考回答 / English answer
不能把普通block barrier当全网格屏障。跨block需明确另一个kernel或适用同步机制。
A block barrier is not a grid-wide barrier. Cross-block dependencies need a separate supported mechanism.GPU reduction里无效lane该直接return吗?
参考回答 / English answer
若之后有需要参与的block同步,不应直接返回;加载单位元并维持必要参与,再掩码输出。
Not if later block synchronization requires participation. I use identity values for inactive inputs and preserve the required control flow.本例第四槽不初始化可能怎样?
参考回答 / English answer
读取未定义或残留值污染归约结果;CPU模型也必须显式初始化,不能假设共享区默认清零。
The reduction can incorporate an indeterminate or stale value. Shared storage must be initialized according to the algorithm.occupancy越高是否性能一定越好?
参考回答 / English answer
不是,适度占用可隐藏延迟,但更多并发可能降低每线程资源或增加争用;要结合实际瓶颈。
Higher occupancy is not a universal performance goal. I measure whether it improves latency hiding without creating other bottlenecks.继续查证
- AMD HIP:Performance guidelines ↗
Memory throughput、shared memory、occupancy
- AMD HIP:C++ language extensions ↗
共享变量、__syncthreads 与执行模型
公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。
接着看已有的图解
- GPU 执行层级与 wave 图解 ↗
辅助对照软件线程与硬件执行单位;旧调试题中的最后一段 wave 隐式同步不能作为正确实现,按本书第 31/36 章证明同步。
这些资料按主题补充本章内容。