硬件与高性能计算:从“程序为什么慢”开始 / Hardware and HPC Starting from Why Programs Are Slow
这套笔记不是硬件名词词典。它只围绕一个问题展开:
一段程序为什么慢,我们能从哪里把时间省下来?
先别急着记 CUDA Core、Warp、HBM 和 NCCL。面对任何性能问题,先问下面四句话:
- 算得慢吗? 算术单元来不及完成工作。
- 取得慢吗? 数据没有及时送到计算单元。
- 等得久吗? CPU、GPU 或多张卡在彼此等待。
- 装得下吗? 内存或显存容量不足。
后面的所有术语,都是为回答这四个问题服务的。
先建立一个心智模型
把计算机想成一家餐厅:
| 计算机概念 | 餐厅里的角色 | 真正要关注的事 |
|---|---|---|
| CPU | 经验丰富的主厨 | 擅长复杂、变化多的任务 |
| GPU | 大量分工一致的厨师 | 擅长同时重复同一种操作 |
| 寄存器 / Cache | 手边和操作台上的食材 | 很快,但放不下太多 |
| 内存 / 显存 | 后厨仓库 | 容量更大,取用更慢 |
| SSD | 楼下的大仓库 | 容量很大,搬运耗时 |
| PCIe / NVLink / 网络 | 传送带和运输道路 | 决定数据搬运速度 |
| CUDA / OpenMP / MPI | 排班和协作规则 | 告诉不同工人如何分工 |
这个类比不负责解释所有细节,但能帮你判断一个新名词处于哪一层、解决什么问题。
推荐路线:先主干,后支线
从源码到硬件的完整路径
source code
-> compiler and linker
-> executable instructions
-> operating-system process and threads
-> CPU pipeline / GPU kernel
-> cache and memory
-> storage, device and network性能问题可能来自任何一层。
源码循环看起来简单,编译器可能没有向量化。 线程很多,调度器可能让它们争用少量核心。
CPU 指令执行很快,Cache Miss 可能让流水线等待。 GPU Kernel 很快,Host/Device 传输可能占总时间大部分。
所以先画端到端路径,再选择工具。
CPU 的定位
CPU 擅长:
- 复杂控制流;
- 低延迟任务;
- 操作系统服务;
- 不规则数据结构;
- 少量强线程;
- 串行关键路径;
- 任务调度与协调。
现代 CPU 使用流水线、乱序执行、分支预测、多级 Cache 和 SIMD。
单核性能受指令依赖、前端、执行端口、分支和内存共同影响。
GPU 的定位
GPU 擅长:
- 大规模数据并行;
- 规则控制流;
- 高吞吐;
- 矩阵和向量;
- 图像与网格;
- 可批量 Kernel;
- 高带宽设备内存。
GPU 通过大量 Warp 隐藏延迟。 线程不足、分支发散和不规则访存都会降低效率。
内存层次
Registers
-> L1 / Shared Memory
-> L2 / LLC
-> DRAM / HBM
-> PCIe / NVLink / Network
-> SSD / Object Storage越靠近计算单元,延迟通常越低、容量越小。
优化要减少数据移动,提高复用,并让访问符合硬件粒度。
延迟与带宽
延迟是单次操作完成需要多久。 带宽是单位时间能传输多少数据。
随机依赖访问更受延迟限制。 连续大块访问更容易受带宽限制。
增加并行度可以隐藏部分延迟,但无法突破持续带宽上限。
算术强度
算术强度表示每搬运一个字节完成多少操作。
arithmetic intensity = operations / bytes低强度 Kernel 常受数据移动限制。 高强度 Kernel 才更可能受计算吞吐限制。
Roofline 用算术强度连接算法和硬件上限。
CPU 并行层次
CPU 并行包括:
- 指令级并行;
- SIMD;
- SMT;
- 多核线程;
- 多插槽 NUMA;
- 多进程;
- 集群 MPI。
层次越多,数据分区和同步越重要。
GPU 并行层次
Grid
-> Block
-> Warp
-> ThreadBlock 是协作和调度边界。 Warp 共享执行指令,分支发散会让路径串行化。
线程通过寄存器、共享内存和全局内存交换或访问数据。
CUDA 的学习重点
学习 CUDA 不只记 API。
需要理解:
- 线程索引;
- 越界保护;
- Block 大小;
- 同步范围;
- 内存空间;
- 合并访问;
- 共享内存;
- Stream;
- 异步拷贝;
- 错误检查;
- Kernel 与端到端测量。
OpenMP 与 SIMD
OpenMP 快速表达共享内存并行。
它适合规则循环和任务,但仍需理解:
- 数据作用域;
- Reduction;
- Scheduling;
- False Sharing;
- NUMA;
- 嵌套并行;
- 线程数量;
- 同步开销。
SIMD 与线程可以组合。 外层分块给线程,内层连续循环给向量单元。
MPI 与集群
MPI 进程拥有独立地址空间,通过消息通信。
partition
-> local compute
-> exchange halo / collective
-> synchronize required dependency
-> continue扩展受串行区、消息延迟、带宽、分区和最慢 Rank 限制。
异构计算
CPU、GPU 和加速器共同完成工作。
设计重点不是“把代码放到 GPU”,而是:
- 哪层拥有数据;
- 哪个设备执行;
- 何时搬运;
- 是否可以重叠;
- 何时同步;
- 如何处理失败;
- 结果在哪里提交。
NPU 与专用加速器
NPU 针对特定张量运算和精度优化能效。
评估时检查算子覆盖、动态形状、内存、编译、回退和生态。 TOPS 是峰值指标,不是模型端到端吞吐。
性能分析工具路线
system timeline
-> stage and wait attribution
-> function / kernel hotspot
-> microarchitecture or memory detail
-> change one factor
-> same end-to-end benchmarkWindows 可使用 WPR/WPA 观察 ETW 时间线。 Intel CPU 可使用 VTune 做 Hotspots、Threading 和 Memory Access。
NVIDIA GPU 使用 Nsight Systems 与 Nsight Compute。 框架级训练可使用 PyTorch Profiler,再下钻到系统工具。
性能实验最小协议
每次实验记录:
- Git 提交;
- 构建参数;
- 硬件;
- 输入;
- 线程/进程/设备;
- 预热;
- 样本;
- 正确性;
- 原始结果;
- 结论范围。
一次运行和一张截图不能证明优化。
典型错误
- 先优化后测量;
- 用 Debug 构建;
- 只看利用率;
- 忽略数据传输;
- 线程越多越好;
- Occupancy 越高越好;
- 把理论峰值当实际目标;
- 用小输入推断大规模;
- 改变正确性换取速度;
- 优化后不做回归。
推荐实验
- 数组顺序与随机访问;
- 工作集跨 Cache;
- AoS 与 SoA;
- 单线程与 SIMD;
- 线程扩展曲线;
- 伪共享;
- NUMA 首次触碰;
- GPU 合并访存;
- Kernel 与传输分项;
- MPI 强弱扩展;
- WPR/WPA 线程等待;
- VTune Hotspots 与 Memory Access。
每个实验都要先写假设,再保存原始样本。
第一遍:建立直觉(约 2~3 小时)
按顺序只读每章的“先抓住直觉”和“升华/核心记忆点”:
- 计算机架构基础:为什么数据位置会影响速度
- 并行计算理论:为什么加机器不等于等比例变快
- GPU 架构:GPU 为什么适合大量重复计算
- CUDA 编程模型:代码如何变成许多 GPU 线程
- CUDA 内存:为什么“搬数据”常比“做计算”更贵
这一遍遇到公式、具体型号参数和完整代码,可以先跳过。
第二遍:学会定位与优化
- 性能分析工具:先用证据判断时间花在哪里
- CUDA 性能优化:针对计算、访存和调度瓶颈下手
- CPU 并行:OpenMP 与 SIMD:别让 CPU 端拖住 GPU
- CPU 与 GPU 协同:让搬运和计算重叠
注意:性能分析应当先于优化。原编号保留,是为了不破坏已有链接。
第三遍:扩展到训练和集群
- MPI 与 HPC 集群:多机多卡为何受通信限制
- 深度学习训练优化:把硬件原理落到训练配置
- NPU 全景:理解不同加速器的共同点与生态差异
- 未来趋势:用“计算、存储、通信”判断新技术
一个概念应该学到什么程度
每遇到一个术语,不要先背定义,按这个顺序理解:
- 它解决什么问题? 例如 Shared Memory 用来减少慢速显存访问。
- 不用它会怎样? 线程会反复从 Global Memory 搬同一批数据。
- 它付出什么代价? Shared Memory 容量有限,还需要线程同步。
- 什么时候该用? 多个线程会重复使用同一小块数据时。
- 最后才是怎么写。 再看 API、关键字和代码模板。
能用自己的话回答前四问,就算真正理解;型号数字、命令参数和 API 可以随用随查。
贯穿全篇的小例子
我们用“向量相加”建立最小模型:
c[0] = a[0] + b[0]
c[1] = a[1] + b[1]
c[2] = a[2] + b[2]
...每一行互不依赖,因此可以分给不同工人同时计算。由它可以逐步引出:
- CPU 单核:一个工人依次完成;
- CPU 多核 / SIMD:少量工人一次处理多份数据;
- GPU:成百上千个轻量线程共同完成;
- CUDA:描述这些 GPU 线程如何编号和访问数据;
- 多 GPU:先切分任务,完成后再交换或汇总结果;
- 性能优化:判断时间到底花在加法、搬运还是等待上。
带着这个小例子进入正文,比一上来想象 175B 模型更容易。大模型训练只是同一组原理在更大规模上的应用。
阅读约定
- 先抓住直觉:不依赖术语的解释,第一次阅读重点看。
- 必须理解:会影响后续章节的主干知识。
- 用到再查:型号参数、API 和工具命令,不要求背诵。
- 深入阅读:解释原理边界,第一遍可以跳过。
准备好了就从 第 0 章:全景学习路线 开始;如果只想快速入门,可直接进入 第 1 章。