CUDA 编程入门:从 Hello GPU 到线程网格模型

在大模型推理与训练日益普及的今天,GPU 已不再是游戏玩家的专属硬件,而是 AI 基础设施的核心。理解 CUDA 编程,是每一位希望深入 AI 系统优化的工程师必经之路。本文从零开始,带你掌握 GPU 并行计算的核心概念与实战代码。一、为什么要用 GPU 计算?

在大模型推理与训练日益普及的今天,GPU 已不再是游戏玩家的专属硬件,而是 AI 基础设施的核心。理解 CUDA 编程,是每一位希望深入 AI 系统优化的工程师必经之路。本文从零开始,带你掌握 GPU 并行计算的核心概念与实战代码。

一、为什么要用 GPU 计算?

CPU 与 GPU 的架构差异

CPU(中央处理器)和 GPU(图形处理器)的设计哲学截然不同。CPU 只有几个核心(通常 4~128 个),但每个核心都非常复杂,拥有大容量缓存、分支预测、乱序执行等机制,旨在最小化单线程延迟。GPU 则拥有数千个简单核心,旨在最大化整体吞吐量

特性CPUGPU
核心数少(4~128)多(数千至上万)
核心复杂度
缓存策略大容量乱序缓存小缓存,共享内存可控
优化目标低延迟高吞吐量
适用场景顺序控制、逻辑分支数据并行、密集计算

GPU 的设计思路是:与其让少量核心飞快完成任务,不如让海量核心同时开工,每个核心处理一小部分数据。这种吞吐量导向的设计,在特定场景下可以带来数十倍甚至上百倍的加速。

GPU 何时能赢?

GPU 并非万能钥匙。它在以下场景表现优异:

  • 数据并行问题:同一段计算逻辑应用于大量独立数据元素,如向量加法、矩阵乘法、图像卷积
  • 高算术强度:计算量远大于数据搬移量,即每字节数据能支撑大量浮点运算
  • 规则化的内存访问模式:连续、对齐的内存访问有利于合并(coalescing),减少带宽浪费

反之,如果任务中存在大量分支分歧、频繁的同步需求、或极低的算术强度,GPU 的优势会被抵消。

二、CUDA 线程层次结构

CUDA 的并行模型以线程网格(Grid) 为核心,采用多层级组织方式,允许多维度的并行度映射。

Grid → Block → Thread

CUDA 将线程组织为一个三维层次结构:

  • Grid(网格):包含多个 Block,一次内核启动对应一个 Grid
  • Block(块):包含多个 Thread,同一块内的线程可以通过共享内存协作,并通过 __syncthreads() 同步
  • Thread(线程):最基本的执行单元

每一级都可以是 1D、2D 或 3D。例如,处理二维图像时,使用 2D 的 Block 和 Grid 可以直观地将线程与像素坐标对应起来。

内置变量

CUDA 在核函数中提供了一组内置变量,用于确定当前线程的身份:

变量含义取值范围
threadIdx.x/y/z当前线程在 Block 内的索引0 ~ blockDim-1
blockIdx.x/y/z当前 Block 在 Grid 内的索引0 ~ gridDim-1
blockDim.x/y/z每个 Block 的维度大小启动时指定
gridDim.x/y/zGrid 的维度大小启动时指定

全局线程索引的计算公式(以 1D 为例):

int globalIdx = blockIdx.x * blockDim.x + threadIdx.x;

Warp 与调度

NVIDIA GPU 以 Warp 为调度单位,一个 Warp 固定包含 32 个线程。同一 Warp 内的线程执行**单指令多线程(SIMT)**模式——它们在同一条指令上协同前进。Warp 调度器在每个时钟周期选择一个就绪的 Warp 执行,通过快速上下文切换隐藏内存访问延迟。

线程分歧(Thread Divergence)

当同一 Warp 内的线程遇到条件分支且执行路径不同时,就会发生线程分歧。硬件会串行化执行各路径,导致性能下降。编写核函数时应尽量保证 Warp 内线程走相同分支,避免 if (threadIdx.x % 2) 这类模式。

三、第一个 CUDA 程序

下面是一个完整的向量加法程序,涵盖了 CUDA 编程的完整流程:分配设备内存、传输数据、启动核函数、回收结果、释放资源。

#include <stdio.h>
#include <cuda_runtime.h>

// 错误检查宏——生产环境必备
#define CUDA_CHECK(call)                                                    \
    do {                                                                    \
        cudaError_t err = call;                                             \
        if (err != cudaSuccess) {                                           \
            fprintf(stderr, "CUDA error at %s:%d: %s\n",                     \
                    __FILE__, __LINE__, cudaGetErrorString(err));           \
            exit(EXIT_FAILURE);                                             \
        }                                                                   \
    } while (0)

// 核函数:每个线程计算 C[i] = A[i] + B[i]
__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() {
    const int N = 1 << 20;          // 1,048,576 个元素
    size_t bytes = N * sizeof(float);

    // 1. 分配主机内存并初始化
    float *h_A = (float *)malloc(bytes);
    float *h_B = (float *)malloc(bytes);
    float *h_C = (float *)malloc(bytes);
    for (int i = 0; i < N; i++) {
        h_A[i] = 1.0f;
        h_B[i] = 2.0f;
    }

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

    // 3. 将数据从主机拷贝到设备(H2D)
    CUDA_CHECK(cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice));

    // 4. 启动核函数
    int threadsPerBlock = 256;
    int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
    vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
    CUDA_CHECK(cudaGetLastError());  // 检查核函数启动错误
    CUDA_CHECK(cudaDeviceSynchronize());

    // 5. 将结果从设备拷贝回主机(D2H)
    CUDA_CHECK(cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost));

    // 6. 验证结果
    for (int i = 0; i < 5; i++) {
        printf("C[%d] = %.1f\n", i, h_C[i]);
    }

    // 7. 释放资源
    CUDA_CHECK(cudaFree(d_A));
    CUDA_CHECK(cudaFree(d_B));
    CUDA_CHECK(cudaFree(d_C));
    free(h_A); free(h_B); free(h_C);

    return 0;
}

编译与运行

nvcc -o vector_add vector_add.cu -O3
./vector_add

核函数启动语法解读

vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);

<<< >>> 是 CUDA 扩展语法,第一个参数是 Grid 维度,第二个参数是 Block 维度。两者都可以是 dim3 类型以支持 2D/3D。第三个参数可选,指定每个 Block 的动态共享内存大小;第四个参数可选,指定执行流(Stream)。

四、显存层次结构

理解 GPU 的显存层次,是写出高性能 CUDA 程序的关键。不同层级的显存拥有截然不同的容量、速度和可见范围。

层次声明方式容量访问速度可见范围生命周期
寄存器(Register)局部变量自动分配~256 KB/SM最快(1 cycle)单个线程线程生命周期
共享内存(Shared Memory)__shared__48228 KB/SM极快(~L1,5 cycles)同 Block 内线程Block 生命周期
常量内存(Constant Memory)__constant__64 KB缓存命中极快全 Grid程序生命周期
纹理/缓存内存纹理对象受缓存限制缓存命中快全 Grid程序生命周期
全局内存(Global Memory)cudaMalloc数 GB~数十 GB最慢(数百 cycles)全 Grid + 主机手动管理
局部内存(Local Memory)寄存器溢出自动分配全局内存子集单个线程线程生命周期

共享内存优化示例

共享内存位于芯片上,速度接近 L1 缓存。经典的**分块矩阵乘法(Tiled Matrix Multiplication)**利用共享内存减少全局内存访问次数。

#define TILE_WIDTH 16

__global__ void matrixMulShared(const float *A, const float *B, float *C,
                                int width) {
    __shared__ float s_A[TILE_WIDTH][TILE_WIDTH];
    __shared__ float s_B[TILE_WIDTH][TILE_WIDTH];

    int bx = blockIdx.x, by = blockIdx.y;
    int tx = threadIdx.x, ty = threadIdx.y;

    int row = by * TILE_WIDTH + ty;
    int col = bx * TILE_WIDTH + tx;

    float Cvalue = 0.0f;

    for (int m = 0; m < (width + TILE_WIDTH - 1) / TILE_WIDTH; m++) {
        // 协作加载 A 和 B 的子块到共享内存
        int aCol = m * TILE_WIDTH + tx;
        int bRow = m * TILE_WIDTH + ty;
        s_A[ty][tx] = (row < width && aCol < width) ? A[row * width + aCol] : 0.0f;
        s_B[ty][tx] = (bRow < width && col < width) ? B[bRow * width + col] : 0.0f;

        __syncthreads();  // 等待全部加载完成

        // 计算子块乘积
        for (int k = 0; k < TILE_WIDTH; k++) {
            Cvalue += s_A[ty][k] * s_B[k][tx];
        }

        __syncthreads();  // 等待计算完成再加载下一轮
    }

    if (row < width && col < width) {
        C[row * width + col] = Cvalue;
    }
}

全局内存访问优化要诀

  • 合并访问(Coalesced Access):Warp 内线程访问连续对齐的内存地址,合并为一次事务
  • 避免非对齐/步幅访问:间隔式访问大幅降低有效带宽
  • 使用 __restrict__ 关键字:告知编译器指针无别名,启用更多优化

五、CUDA 流(Streams)与异步执行

默认情况下,CUDA 核函数和内存拷贝在流 0(默认流)上执行,且与主机端是同步的。通过创建非默认流,可以实现异步执行,使计算与数据传输重叠。

流的创建与使用

#include <stdio.h>
#include <cuda_runtime.h>

__global__ void scaleKernel(float *data, float scale, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        data[i] *= scale;
    }
}

int main() {
    const int N = 1 << 20;
    size_t bytes = N * sizeof(float);
    int numStreams = 4;

    // 分配页锁定内存(允许异步传输)
    float *h_data;
    cudaMallocHost(&h_data, bytes);

    float *d_data;
    cudaMalloc(&d_data, bytes);

    // 创建多个流
    cudaStream_t streams[numStreams];
    for (int i = 0; i < numStreams; i++) {
        cudaStreamCreate(&streams[i]);
    }

    int chunkSize = N / numStreams;
    int threadsPerBlock = 256;
    int blocksPerGrid = (chunkSize + threadsPerBlock - 1) / threadsPerBlock;

    // 分块异步执行:拷贝 → 计算 → 拷贝回
    for (int i = 0; i < numStreams; i++) {
        int offset = i * chunkSize;
        size_t chunkBytes = chunkSize * sizeof(float);

        // 异步 H2D 拷贝
        cudaMemcpyAsync(d_data + offset, h_data + offset,
                        chunkBytes, cudaMemcpyHostToDevice, streams[i]);

        // 异步启动核函数
        scaleKernel<<<blocksPerGrid, threadsPerBlock, 0, streams[i]>>>
            (d_data + offset, 2.0f, chunkSize);

        // 异步 D2H 拷贝
        cudaMemcpyAsync(h_data + offset, d_data + offset,
                        chunkBytes, cudaMemcpyDeviceToHost, streams[i]);
    }

    // 等待所有流完成
    for (int i = 0; i < numStreams; i++) {
        cudaStreamSynchronize(streams[i]);
    }

    // 清理
    for (int i = 0; i < numStreams; i++) {
        cudaStreamDestroy(streams[i]);
    }
    cudaFree(d_data);
    cudaFreeHost(h_data);

    return 0;
}

重叠执行的原理

现代 GPU 通常具备独立的拷贝引擎(Copy Engine)计算引擎(Compute Engine)。通过 cudaMemcpyAsync 配合页锁定内存(Pinned Memory),主机到设备的传输可以异步进行。如果数据传输引擎和计算引擎互不干扰,理想情况下可以实现"一边传输下一块数据,一边计算当前块"的流水线,最大限度提升吞吐量。

⚠️ 异步拷贝必须使用页锁定主机内存cudaMallocHostcudaHostAlloc),普通 malloc 内存无法发起真正的 DMA 传输。

六、GPU 架构纵览

Streaming Multiprocessor(SM)

SM 是 GPU 的核心计算单元。每个 SM 包含:

  • 一组 CUDA Core(执行整数和浮点运算)
  • 特殊功能单元(SFU,处理三角函数、开方等)
  • Load/Store 单元
  • 寄存器文件
  • 共享内存 / L1 缓存
  • Warp 调度器

一个 GPU 芯片通常集成数十到上百个 SM,例如 A100 有 108 个 SM,H100 有 132 个 SM。

CUDA Core 与 Tensor Core

  • CUDA Core:通用标量计算单元,支持 FP32、FP64、INT 运算
  • Tensor Core:专用矩阵乘加(MMA)加速器,从 Volta 架构引入。在 FP16/BF16/INT8 精度下提供极高吞吐,是深度学习训练与推理加速的核心

Tensor Core 支持 mma.sync.aligned 等指令,也可通过 cuBLAS、CUTLASS 等库间接调用。

各代架构演进

架构代号代表显卡Compute Capability关键特性
VoltaV1007.0首款 Tensor Core,独立 L1/共享内存,NVLink 2.0
AmpereA100, RTX 30 系8.0 / 8.6第三代 Tensor Core(支持稀疏性加速),MIG 多实例
HopperH1009.0第四代 Tensor Core,Transformer Engine(FP8 自动混合精度),DPX 指令
BlackwellB200, RTX 50 系10.0第五代 Tensor Core,第二代 Transformer Engine,支持 FP4/FP6

Compute Capability(计算能力)

Compute Capability 以 x.y 格式标识 GPU 的硬件特性版本。主版本号对应架构世代(如 8.x = Ampere),次版本号代表特定 SKU 的小幅差异。开发时需关注目标硬件的 Compute Capability,以确定支持的指令集和最大资源限制。

# 查看本机 GPU 的 Compute Capability
nvidia-smi
/usr/local/cuda/extras/demo_suite/deviceQuery

总结

知识点核心要点
GPU 优势吞吐量导向,数据并行场景
线程模型Grid/Block/Thread 三级层次,Warp 为调度单位
编程流程分配 → 拷贝 → 核函数 → 拷贝回 → 释放
显存层次寄存器 > 共享内存 > 常量内存 > 全局内存
流与异步分块重叠计算与传输,使用页锁定内存
硬件演进Tensor Core 是深度学习加速关键,架构持续迭代

CUDA 编程的学习曲线并不陡峭,但要写出接近硬件极限的高性能代码,则需要深入理解 Warp 行为、显存层次、以及各代架构的微架构差异。本文提供的向量加法、分块矩阵乘法和流异步执行三个完整示例,构成了后续深入 CUDA 优化的坚实基础。建议读者在真实 GPU 上编译运行这些代码,通过 nvprof 或 Nsight Compute 分析工具观察性能瓶颈,逐步进阶到更高级的优化技法。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. 模型量化技术详解:INT8、FP16 与混合精度推理
  2. 模型剪枝与知识蒸馏:从压缩到加速全链路
  3. 推理引擎终极对比:TensorRT vs ONNX Runtime vs OpenVINO