1. 并行计算概念
并行计算是指在拥有多个计算资源(例如多个核心或多台计算机)时,将一个大问题拆分为多个小问题,并在不同计算资源上同时处理这些子问题。
并行计算主要分为两个方向:
- 计算机架构:关注硬件层面如何支持并行性。
- 并行程序设计:关注如何利用计算机架构的能力并发地解决问题。
现代芯片设计越来越倾向于将多个核心集成到一片芯片上,因此并行计算已经成为计算机体系结构与程序设计中的重点。
2. 并行性与数据并行
所有并行程序一定是并发程序,但包含并发任务的程序不一定是并行程序。
并行方式通常可以分为两类:
- 任务并行:当多个任务或函数可以独立并大规模地并行执行时,称为任务并行。重点在于对任务进行分配。
- 数据并行:当可以同时处理大量数据时,称为数据并行。重点在于对数据进行分配。
CUDA 编程非常适合解决数据并行问题。对于大数据集处理类应用,数据并行模型可以有效提升计算单元的吞吐能力,并将数据映射到并行线程上。
2.1 数据并行程序设计
数据并行程序设计的核心思想是:将数据按照线程进行划分,使每个线程处理其中一部分数据。
常见的数据划分方式包括:
- 块划分:每个线程作用于一段连续数据,通常每段数据大小相同。
- 周期划分:每个线程处理数据的多个部分,线程之间按周期分配数据块。
块划分中,一组连续数据被分配到一个块内,每个数据块通常由一个线程按任意次序处理,且线程在同一时间通常只处理一个数据块。
周期划分中,相邻线程处理相邻的数据块,每个线程可以处理多个数据块。为一个线程分配新的数据块时,意味着要跳过与现有线程相同数量的数据块。
3. 计算机架构
3.1 常见并行架构
- SIMD:单指令流处理多个数据流。典型例子是向量机。
- MISD:较少见,多个指令流处理同一个数据流。
- MIMD:多个核心使用多个指令流异步处理多个数据流,实现空间上的并行性。
许多 MIMD 架构还包含 SIMD 执行的子组件。
3.2 架构层次与扩展方向
并行系统的演进通常体现在以下几个方面:
- 降低延迟(微秒级)。
- 提高带宽(MB/s、GB/s)。
- 提高吞吐量(GFLOPS,表示每秒十亿次浮点运算)。
按内存组织方式,还可以进一步划分为:
- 分布式内存的多节点系统。
- 共享内存的多处理器系统。
在多节点系统中,大型计算引擎由多个通过网络连接的处理器组成。每个处理器拥有自己的本地内存,处理器之间通过网络通信。
在多处理器架构中,系统规模通常从双处理器到几十个甚至几百个处理器不等。这些处理器要么关联同一物理内存,要么共享低延迟链路(如 PCI-Express 或 PCIe)。共享内存意味着共享地址空间,但并不一定意味着单独的物理内存。此类系统既包括多核单芯片系统,也包括由多个芯片组成的计算机系统。如今,多核架构已经基本取代单核架构。
4. GPU 架构
GPU 代表一种众核架构,通常结合以下特征:
- 多线程
- MIMD(多指令多数据)
- SIMD(单指令多数据)
- 指令级并行
NVIDIA 将这种架构称为 SIMT(单指令多线程)。
尽管可以用“多核”和“众核”来区分 CPU 和 GPU 的架构,但二者本质上是完全不同的:
- CPU 核心较重,适合处理复杂控制逻辑,优化串行程序执行。
- GPU 核心较轻,适合处理控制逻辑较简单的数据并行任务,更强调并行程序的吞吐量。
5. 异构计算
异构计算可以简单理解为:CPU 负责控制,GPU 负责执行重计算任务。
典型的异构计算节点通常包括:
- 两个多核 CPU 插槽
- 两个或更多众核 GPU
GPU 本质上可以看作 CPU 的协处理器,也是最常见的硬件加速器之一。
在 CUDA 语境中:
- Host:CPU 所在的位置。
- Device:GPU 所在的位置。
主机代码运行在 CPU 上,负责应用初始化以及管理设备端的环境、代码和数据。 设备代码运行在 GPU 上,用于提升并行计算的执行速度。
NVIDIA 的 GPU 计算平台包括:Tegra、GeForce、Quadro、Tesla。
NVIDIA 从 2010 年的 Fermi(首款完整 GPU 计算架构)到 2012 年的 Kepler,再到嵌入式 Tegra K1,不断推动 GPU 在高性能计算领域的应用和算力提升,覆盖地震、生化、气候、金融等场景。
5.1 GPU 特征与性能指标
GPU 的常见特征指标:
- CUDA 核心数量
- 内存大小
GPU 的核心性能指标:
- 峰值计算性能
- 内存带宽
其中,峰值计算性能(GFLOPS/TFLOPS)和内存带宽(GB/s)是衡量 GPU 计算能力的两个核心指标。Fermi 到 Kepler 架构在这两方面均有显著提升。
5.2 计算能力
NVIDIA 使用计算能力主版本号标识 GPU 架构世代:
- 1 = Tesla
- 2 = Fermi
- 3 = Kepler
相同主版本号通常表示相同的核心架构。
5.3 GPU 与 CPU 的差异
- CPU 适合处理数据规模较小、并行性较低的任务。
- GPU 更适合处理计算密集型、并行性高的任务。
因此,程序通常会将计算密集型部分交给 GPU,而将串行部分保留在 CPU 上执行。
CPU 的线程通常是重量级的,上下文切换成本较高,因此可支持的线程数量远少于 GPU。四核 CPU 通常可同时运行 16 个线程,如果支持超线程则可进一步翻倍;而 GPU 在每个多处理器上最多可支持 1536 个线程,16 个多处理器的 GPU 最多可支持约 24000 个线程。
6. CUDA 历史
| 时间 | 版本 / 架构 | 主要特性 |
|---|---|---|
| 2006 | G80 / CUDA 首发 | GTX 8800,首个支持通用计算的 GPU |
| 2008 | CUDA 2.0 | 支持双精度 |
| 2010 | CUDA 3.0 / Fermi | 支持 C++ |
| 2012 | CUDA 5.0 / Kepler | Dynamic Parallelism |
| 2017 | CUDA 9.0 / Volta | 引入 Tensor Core |
| 2020 | CUDA 11.0 / Ampere | MIG 多实例 |
| 2024 | CUDA 12.x / Hopper / Blackwell | 支持 FP8 / FP4 |
FP8 和 FP4 是极低精度浮点格式,用于加速 AI 推理和训练,并节省显存与带宽。
7. CUDA 驱动 API 与运行时 API
CUDA 驱动 API 和运行时 API 在性能上没有明显差异,但二者互斥。
CUDA 编程真正影响性能的因素,是线程组织方式和内存使用方式,而不是 API 选择本身。
nvcc 会将 CUDA 代码拆分为两部分:
- 主机代码:标准 C,由主机 C 编译器编译。
- 设备代码:核函数,由 CUDA C 扩展标记,并由 nvcc 编译。
在链接阶段,会加入 CUDA 运行时库,以处理内核调用和 GPU 操作。
8. CUDA 编程结构
一个典型的 CUDA 编程结构包括以下 5 个步骤:
- 分配 GPU 内存。
- 将数据从 CPU 内存拷贝到 GPU 内存。
- 调用 CUDA 内核函数完成指定运算。
- 将结果从 GPU 拷回 CPU 内存。
- 释放 GPU 内存空间。
9. 编写 CUDA 的要求
编写 CUDA 程序时,建议具备以下基础:
- 熟悉 GPU 结构。
- 了解 CPU 架构,例如数据局部性(时间局部性、空间局部性)等原理。