异构计算与CUDA概述

欢迎进入《异构计算CUDA并行编程深度精讲》的第一章。在传统观念中,CPU 的性能提升主要依赖主频提高与核心数增加,但功耗墙与存储墙的出现让这条路越走越窄。与此同时,GPU 凭借数千个轻量级核心在图像渲染中积累的并行吞吐能力,逐渐被用于通用计算。异构计算由此成为高性能计算领域的主流范式——让不同特性的处理器各司其职,协同完成复杂任务。

1.1 异构计算的核心思想

异构计算(Heterogeneous Computing)指在一个系统中集成多种指令集架构或微架构不同的处理器,由主机端(Host)与设备端(Device)协同工作。在典型的 CUDA 系统中,CPU 作为主机负责逻辑控制与串行任务,GPU 作为设备负责大规模数据并行计算。两者通过 PCIe 总线或 NVLink 互联,各自拥有独立的存储空间。

关键认知:异构计算不是简单地把任务丢给 GPU,而是将任务拆分为“适合串行控制”的部分与“适合并行吞吐”的部分。CPU 擅长分支预测、乱序执行与低延迟响应;GPU 擅长大量线程的吞吐计算,但单个线程延迟较高。合理划分任务边界,才能发挥异构系统的整体效率。

从硬件视角看,GPU 内部包含多个流多处理器(SM),每个 SM 又包含若干 CUDA 核心、共享内存、寄存器堆与调度器。这种层级化结构决定了 CUDA 编程模型必须暴露线程层级、内存层级与同步语义,才能让开发者精细控制硬件资源。

异构计算系统架构与 CUDA 编程模型映射 CPU (Host) 控制逻辑 · 串行任务 Core 0 Core 1 Core 2 Core 3 共享缓存 (LLC) 内存控制器 GPU (Device) 大规模并行 · 吞吐计算 SM 0 CUDA 核心 ×N 共享内存 寄存器堆 SM 1 CUDA 核心 ×N 共享内存 寄存器堆 SM 2 CUDA 核心 ×N 共享内存 寄存器堆 SM 3 CUDA 核心 ×N 共享内存 寄存器堆 ··· ··· ··· ··· L2 缓存(设备端统一缓存) 全局显存 (Global Memory / VRAM) 主机内存 (Host Memory) PCIe 或 NVLink 数据拷贝 内核启动 CUDA 编程模型映射关系 线程层级 Grid → Block → Thread 对应 SM 调度粒度 内存层级 全局 → 共享 → 私有 对应显存/SMEM/寄存器 同步语义 __syncthreads() 块内线程同步 内核函数 __global__ kernel() CPU 启动,GPU 执行

1.2 CUDA 编程模型与执行流程

CUDA(Compute Unified Device Architecture)是 NVIDIA 提供的异构并行计算平台与编程模型。其核心抽象包括:线程层级结构、内存层级结构与同步语义。一个典型的 CUDA 程序由主机代码与设备代码组成,设备代码通过 __global__ 修饰的内核函数(Kernel)表达。

内核启动时,开发者需要指定网格(Grid)与线程块(Block)的维度。GPU 硬件将线程块调度到 SM 上执行,每个 SM 可以同时驻留多个线程块。线程块内的线程通过共享内存协作,并通过 __syncthreads() 进行同步。这种两层并行结构既保证了可扩展性(不同 GPU 的 SM 数量不同),又提供了细粒度的数据共享机制。

标准 CUDA 执行流程

  1. 分配设备内存:调用 cudaMalloc 在 GPU 显存中分配输入/输出缓冲区。
  2. 数据拷贝:使用 cudaMemcpy 将输入数据从主机内存传输到设备显存。
  3. 启动内核:以 kernel<<<grid, block>>>(args) 语法启动并行计算。
  4. 同步与回拷:调用 cudaDeviceSynchronize 等待内核完成,再将结果拷回主机。
  5. 释放资源:调用 cudaFree 释放设备内存。
// 典型 CUDA 程序骨架
#include <cuda_runtime.h>
#include <stdio.h>

__global__ void vectorAdd(const float *A, const float *B, float *C, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        C[i] = A[i] + B[i];
    }
}

int main() {
    int n = 1 << 20;
    size_t bytes = n * sizeof(float);

    // 1. 分配设备内存
    float *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, bytes);
    cudaMalloc(&d_B, bytes);
    cudaMalloc(&d_C, bytes);

    // 2. 主机数据准备与拷贝
    float *h_A = (float*)malloc(bytes);
    float *h_B = (float*)malloc(bytes);
    // ... 初始化 h_A, h_B ...
    cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);

    // 3. 启动内核
    int threads = 256;
    int blocks = (n + threads - 1) / threads;
    vectorAdd<<<blocks, threads>>>(d_A, d_B, d_C, n);

    // 4. 同步并回拷结果
    cudaDeviceSynchronize();
    cudaMemcpy(h_A, d_C, bytes, cudaMemcpyDeviceToHost);

    // 5. 释放资源
    cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
    free(h_A); free(h_B);
    return 0;
}

1.3 典型应用场景与性能特征

异构计算并非万能药。以下场景最适合 CUDA 加速:

应用领域 并行特征 典型加速比
矩阵乘法 / 线性代数 数据高度规整,计算密度大 10× ~ 100×
图像处理 / 计算机视觉 像素级独立操作,天然并行 20× ~ 80×
深度学习训练与推理 卷积/矩阵运算为主,批量并行 5× ~ 50×
分子动力学模拟 粒子间相互作用,邻域列表 10× ~ 60×
金融风险分析(蒙特卡洛) 大量独立随机路径 30× ~ 200×

判断准则:如果任务满足“数据并行、计算密集、算术强度高、分支少”四个特征,CUDA 通常能带来显著收益。反之,如果任务以串行依赖为主、分支发散严重或数据量过小(无法掩盖传输开销),则 CPU 可能更合适。

1.4 常见误区与学习路径

误区一:认为 GPU 主频高所以单线程快。实际上 GPU 单核主频低于 CPU,其优势在于数千线程的并行吞吐。若只写单线程代码,GPU 反而更慢。

误区二:忽略数据传输开销。PCIe 带宽远低于显存带宽,频繁的主机-设备拷贝会严重拖累性能。应尽量减少传输次数,或使用零拷贝/统一虚拟内存技术。

误区三:过度使用共享内存。共享内存容量有限(通常几十 KB/SM),使用不当会导致线程块驻留数下降,反而降低并行度。

本课程共 30 章,后续将依次深入 CUDA 线程模型、内存优化、原子操作、流与并发、多 GPU 编程、性能分析工具(Nsight)、以及若干实战项目。每一章都会配合可运行的代码示例与性能剖析,帮助你建立从“能跑”到“跑得快”的完整能力。