1CUDA 并行程序设计入门(第2版)

为什么选择 CUDA

GPU 计算的崛起

GPU架构CUDA历史并行计算动机性能对比

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第1章 为什么选择 CUDA - GPU 计算的崛起(The Rise of GPU Computing)

一、导读

1.1 本章学习目标

通过本章的学习,你将能够:

理解 CPU 与 GPU 在架构设计上的根本差异及其适用场景

掌握并行计算的基本概念和分类方法(SIMD、MIMD 等)

了解 GPU 计算的历史发展脉络和关键里程碑

认识 CUDA 平台的核心优势和技术特点

能够分析哪些类型的问题适合使用 GPU 加速

理解摩尔定律的局限性以及异构计算的必要性

掌握评估计算任务并行潜力的基本方法

了解 GPU 计算在现代科学计算、人工智能和图形处理中的应用现状

1.2 为什么本章如此重要

在当今的计算世界中,GPU 已经从一个纯粹的图形渲染引擎演变为通用并行计算的核心力量。从深度学习模型的训练到气候模拟,从金融风险分析到医学影像处理,GPU 正在重塑我们解决复杂计算问题的方式。

理解为什么选择 CUDA 不仅仅是学习一门技术,更是理解现代计算范式的转变。CPU 的设计哲学是"快速执行少量线程",而 GPU 的设计哲学是"同时执行海量线程"。这种根本性的差异决定了它们在解决不同类型问题时的效率天壤之别。

对于程序员而言,掌握 GPU 编程意味着获得了一种全新的思维方式。传统的串行编程思维在面对大规模数据并行时往往力不从心,而 GPU 编程要求我们从并行的角度重新审视问题。本章将为你奠定这一思维转变的理论基础。

此外,随着摩尔定律逐渐接近物理极限,单纯依靠提升 CPU 主频来获得性能提升的时代已经结束。异构计算——将 CPU 的灵活性与 GPU 的吞吐量相结合——已成为提升计算性能的主流方向。CUDA 作为最成熟的 GPU 计算平台之一,其重要性不言而喻。

1.3 核心问题引导

在深入本章内容之前,请思考以下问题:

为什么同样的计算任务在 GPU 上可能比在 CPU 上快数百倍?

GPU 的"众核"架构与 CPU 的"多核"架构有什么本质区别?

什么样的计算问题天然适合 GPU 加速?什么样的问题不适合?

CUDA 与其他 GPU 计算框架(如 OpenCL、DirectCompute)相比有何优势?

为什么 NVIDIA 能够在 GPU 计算领域建立主导地位?

GPU 内存带宽对计算性能的影响有多大?

如何判断一个应用程序是否值得进行 GPU 加速优化?

GPU 计算的未来发展趋势是什么?它将如何改变软件开发的格局?


二、核心概念详解

2.1 CPU 与 GPU 的架构差异

要理解为什么选择 CUDA,首先必须深入理解 CPU 和 GPU 在设计理念上的根本差异。这两种处理器虽然都执行指令,但它们的目标和优化方向截然不同。

CPU:延迟优化的大师

中央处理器(CPU)的设计目标是最小化单个任务的执行延迟。想象一下,CPU 就像是一位精通多种技能的专家,他能够快速地完成各种复杂的任务,包括条件分支、中断处理、虚拟内存管理等。CPU 拥有:

  • 强大的控制单元:能够高效地处理复杂的控制流,包括深层嵌套的条件语句和循环
  • 大容量的缓存层次结构:L1、L2、L3 缓存逐级增大,旨在减少访问主内存的延迟
  • 复杂的指令调度机制:乱序执行、分支预测、 speculative execution 等技术最大限度地提高指令级并行度
  • 较少的核心数量:通常 4-16 个高性能核心,每个核心都能独立执行复杂的任务序列

CPU 擅长的是需要频繁决策、逻辑复杂、数据依赖性强的任务。例如操作系统内核、数据库查询优化器、编译器等工作负载,这些任务的特点是控制流复杂、难以预测,但每个步骤都需要精确执行。

GPU:吞吐量优化的引擎

图形处理器(GPU)的设计目标是最大化单位时间内完成的工作总量,即吞吐量。GPU 就像是一支庞大的工人队伍,每个人只能做相对简单的工作,但当成千上万的工人同时工作时,整体产出惊人。GPU 拥有:

  • 海量的并行处理单元:现代 GPU 拥有数千个流处理器(Streaming Processors),可以同时执行大量相同的操作
  • 简化的控制逻辑:每个核心的控制单元相对简单,牺牲了处理复杂控制流的能力以换取更多的计算单元
  • 高带宽内存系统:GPU 显存带宽通常是 CPU 主存带宽的数倍甚至十倍,这对于数据密集型计算至关重要
  • SIMT 执行模型:单指令多线程(Single Instruction, Multiple Threads)架构允许同一指令在多个数据元素上并行执行

GPU 擅长的是数据并行任务,即对大量数据元素执行相同或相似的操作。图形渲染就是一个典型例子:屏幕上的每个像素都需要进行类似的颜色计算、光照计算等操作,这些操作彼此独立,可以完美并行。

架构对比图示

CPU 架构示意:
┌─────────────────────────────────────┐
│         Complex Control Unit        │
├──────────┬──────────┬───────────────┤
│  Core 0  │  Core 1  │   ... Core N  │
│ (Heavy)  │ (Heavy)  │   (Heavy)     │
├──────────┴──────────┴───────────────┤
│      Large Cache Hierarchy          │
│    L1 → L2 → L3 → Main Memory       │
└─────────────────────────────────────┘

GPU 架构示意:
┌─────────────────────────────────────┐
│     Simplified Control Units        │
├───┬───┬───┬───┬───┬───┬───┬───┬───┤
│SP │SP │SP │SP │SP │SP │SP │SP │...│  ← 数千个流处理器
├───┴───┴───┴───┴───┴───┴───┴───┴───┤
│     High-Bandwidth Memory Bus       │
│         GDDR6 / HBM                 │
└─────────────────────────────────────┘

这种架构差异导致了两者在性能特征上的显著不同。对于一个包含 100 万次独立加法运算的任务,CPU 可能需要串行或有限并行地执行这些加法,而 GPU 可以同时启动数千个线程来并行完成这些运算。

2.2 并行计算的分类与 Flynn 分类法

理解并行计算需要对计算任务的并行特性有清晰的认识。Michael Flynn 在 1966 年提出的分类法至今仍是理解并行架构的基础框架。

SISD(Single Instruction, Single Data)

单指令单数据是最传统的计算模式,也是传统串行程序的执行方式。每条指令作用于单个数据元素,下一条指令再作用于下一个数据元素。大多数 CPU 程序在宏观上表现为 SISD 模式,尽管现代 CPU 通过流水线技术在微观层面实现了一定程度的并行。

SIMD(Single Instruction, Multiple Data)

单指令多数据是 GPU 计算的核心模式。同一条指令同时作用于多个数据元素。例如,将两个长度为 1024 的数组相加,在 SIMD 模式下,一条加法指令可以同时处理多个数组元素的配对相加。

GPU 的 SIMT(Single Instruction, Multiple Threads)是 SIMD 的扩展变体。在 SIMT 中,多个线程执行相同的指令序列,但每个线程可以有自己的程序计数器和寄存器状态,允许一定程度的线程级独立性。

MISD(Multiple Instruction, Single Data)

多指令单数据在实际硬件中很少见,主要用于某些特殊的容错计算场景,其中多个不同的处理单元对同一数据执行不同的操作以进行验证。

MIMD(Multiple Instruction, Multiple Data)

多指令多数据是现代多核 CPU 集群的典型模式。每个处理单元可以独立执行不同的指令序列,处理不同的数据。这种模式提供了最大的灵活性,但也带来了同步和通信的复杂性。

GPU 计算中的并行维度

在 GPU 编程中,我们主要关注两种并行性:

数据并行(Data Parallelism):相同操作应用于不同数据元素。这是 GPU 最擅长的领域,例如图像处理、矩阵运算、物理模拟等。

任务并行(Task Parallelism):不同操作可以同时进行。GPU 在这方面的能力有限,因为它的核心设计是针对数据并行的。

理解你的问题属于哪种并行类型,是决定是否使用 GPU 加速的关键第一步。

2.3 GPU 计算的历史演进

GPU 计算的发展并非一蹴而就,而是经历了数十年的技术积累和应用驱动。

早期阶段:固定功能管线(1990s)

早期的 GPU 是纯粹的图形加速器,提供固定的渲染管线功能。程序员无法直接控制 GPU 的执行逻辑,只能通过 API(如 OpenGL、Direct3D)配置预定义的渲染状态。这一阶段的 GPU 虽然已经具备并行处理能力,但缺乏通用编程的灵活性。

可编程着色器时代(2001-2006)

NVIDIA GeForce 3 引入了可编程顶点着色器,随后 GeForce FX 系列引入了像素着色器。程序员可以使用汇编-like 语言编写着色器程序,实现自定义的图形效果。这一时期的关键技术突破包括:

  • 顶点着色器:允许对几何数据进行自定义变换
  • 像素着色器:允许对每个像素进行自定义颜色计算
  • 浮点精度支持:从整数运算扩展到浮点运算,为通用计算奠定基础

然而,着色器模型仍然受到图形管线的严格限制,程序必须在渲染管线的特定阶段执行,且无法自由访问内存。

GPGPU 时代(2003-2006)

通用 GPU 计算(General-Purpose computing on GPU)的先驱们发现,可以将非图形计算问题"伪装"成图形问题来处理。例如,将数据编码为纹理,将计算编码为片段着色器,将结果读取回内存。这种方法虽然可行,但极其繁琐且效率低下:

  • 需要将数据映射到图形原语(三角形、纹理)
  • 计算结果需要通过帧缓冲区读取
  • 缺乏直接的内存访问机制
  • 调试困难,错误信息晦涩

尽管存在诸多限制,GPGPU 证明了 GPU 在通用计算领域的巨大潜力,特别是在科学计算、信号处理等领域。

CUDA 革命(2007 至今)

2006 年,NVIDIA 推出了 Tesla 架构,这是第一个真正为通用计算设计的 GPU 架构。2007 年,CUDA(Compute Unified Device Architecture)正式发布,标志着 GPU 计算进入了一个全新的时代。

CUDA 的核心创新包括:

统一的编程模型:提供 C/C++ 扩展,使程序员可以直接编写 GPU 代码,无需借助图形 API

直接内存访问:GPU 可以直接读写全局内存,无需通过纹理或帧缓冲区

线程抽象:引入线程块(Thread Block)和网格(Grid)的概念,简化并行线程的管理

共享内存:提供片上高速缓存,用于线程间通信和数据复用

原子操作:支持线程安全的内存更新操作

CUDA 的发布极大地降低了 GPU 编程的门槛,使得更多领域的开发者能够利用 GPU 的计算能力。随后的几年中,CUDA 生态系统迅速壮大,涵盖了库、工具、框架等多个层面。

竞争与标准化

随着 CUDA 的成功,其他厂商也推出了自己的 GPU 计算方案:

  • OpenCL(2008):由 Khronos Group 制定的开放标准,支持跨平台 GPU 计算
  • DirectCompute(2009):微软 DirectX 11 引入的 GPU 计算 API
  • AMD ROCm(2016):AMD 推出的开源 GPU 计算平台
  • oneAPI(2020):Intel 推出的统一编程模型

尽管存在多种选择,CUDA 凭借其成熟的生态系统、丰富的库支持和广泛的社区基础,仍然是 GPU 计算领域的事实标准。

2.4 CUDA 平台的核心组成

CUDA 不仅仅是一个编程语言扩展,它是一个完整的并行计算平台,包含多个层次的组件:

CUDA Toolkit

  • nvcc 编译器:将 CUDA C/C++ 代码编译为 GPU 可执行的二进制代码
  • CUDA Runtime API:提供设备管理、内存管理、内核启动等高级功能
  • CUDA Driver API:提供更底层的设备控制接口
  • 性能分析工具:nsight、nvprof 等工具用于分析和优化 CUDA 程序

CUDA Libraries

  • cuBLAS:线性代数基本子程序库
  • cuFFT:快速傅里叶变换库
  • cuRAND:随机数生成库
  • Thrust:类似 STL 的并行算法库
  • cuDNN:深度神经网络库
  • NCCL:多 GPU 通信库

开发环境支持

  • IDE 集成(Visual Studio、Eclipse 等)
  • 调试器支持
  • 性能分析可视化

这种多层次的架构使得开发者可以根据需求选择合适抽象级别的工具,从底层的手动优化到高层的库调用,都有相应的支持。

2.5 Amdahl 定律与 Gustafson 定律

在评估 GPU 加速的潜力时,两个重要的理论模型提供了指导:

Amdahl 定律

Gene Amdahl 在 1967 年提出,程序的加速比受限于其串行部分的比例。如果一个程序中有比例为 P 的部分可以并行化,那么在使用无限多处理器时,最大加速比为 1/(1-P)。

例如,如果一个程序有 90% 的代码可以并行化,那么即使使用无限多的处理器,最大加速比也只有 10 倍。这一定律提醒我们,优化串行部分同样重要。

Gustafson 定律

John Gustafson 提出了不同的观点:随着问题规模的增大,并行部分的比例也会增加。在实际应用中,我们往往不是用更多处理器来解决相同的问题,而是用更多处理器来解决更大的问题。

这两个定律从不同角度揭示了并行计算的本质。在实践中,我们需要根据具体应用场景选择合适的视角。


三、程序员的视角

3.1 在实际项目中的应用

GPU 计算已经在众多领域展现出巨大的价值:

深度学习

深度学习是 GPU 计算最成功的应用领域之一。神经网络的训练涉及大量的矩阵乘法和卷积运算,这些都是典型的并行计算任务。NVIDIA 的 cuDNN 库为常见的深度学习操作提供了高度优化的实现,使得训练时间从数周缩短到数小时。

科学计算

从分子动力学模拟到天体物理计算,从流体动力学到量子化学,GPU 加速使得以前需要超级计算机才能完成的计算可以在工作站上完成。许多科学计算软件(如 GROMACS、LAMMPS、NAMD)都提供了 GPU 加速版本。

图像处理与计算机视觉

实时视频处理、图像增强、特征提取等任务都可以从 GPU 加速中受益。OpenCV 等库提供了 GPU 加速模块,使得实时计算机视觉应用成为可能。

金融建模

期权定价、风险评估、蒙特卡洛模拟等金融计算任务涉及大量独立的数值计算,非常适合 GPU 加速。一些金融机构报告称,使用 GPU 后,原本需要数小时的计算可以在几分钟内完成。

游戏开发

除了传统的图形渲染,现代游戏引擎也利用 GPU 进行物理模拟、AI 决策、粒子系统等计算。这使得游戏世界更加真实和动态。

3.2 常见陷阱与误区

误区一:所有程序都适合 GPU 加速

事实是,只有具有足够并行度的程序才能从 GPU 加速中受益。如果程序的控制流复杂、数据依赖性强、或者计算量本身很小,GPU 加速可能反而导致性能下降。

误区二:GPU 总是比 CPU 快

GPU 的优势在于吞吐量,而非延迟。对于需要快速响应单个请求的场景,CPU 可能表现更好。此外,数据传输开销(CPU 到 GPU 和反向)可能抵消计算加速带来的收益。

误区三:GPU 编程极其困难

虽然 GPU 编程确实有其特殊性,但 CUDA 提供了相对友好的编程模型。对于熟悉 C/C++ 的程序员来说,学习曲线并不陡峭。更重要的是,许多常见计算任务已经有现成的库可用,无需从头编写 CUDA 代码。

陷阱:忽视内存访问模式

GPU 的性能很大程度上取决于内存访问的效率。不合理的内存访问模式(如非合并访问)可能导致性能下降数个数量级。这是初学者最常遇到的性能问题。

3.3 与其他层级的联系

GPU 计算处于计算机体系结构的多个层级之间:

  • 硬件层:理解 GPU 的架构特性(缓存层次、内存带宽、执行单元)对于编写高效代码至关重要
  • 系统层:操作系统对 GPU 资源的管理、驱动程序的作用、PCIe 总线的影响等
  • 算法层:如何将问题分解为适合 GPU 执行的并行任务
  • 应用层:如何将 GPU 加速集成到完整的应用程序中

本章主要关注算法层和系统层的概念,后续章节将深入硬件层的具体细节。


四、代码实战

4.1 CPU 与 GPU 性能对比示例

以下代码展示了同一个向量加法任务在 CPU 和 GPU 上的实现对比:

cpp
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// CPU 版本的向量加法
void vectorAddCPU(float *a, float *b, float *c, int n) {
    for (int i = 0; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}

// GPU 内核函数
__global__ void vectorAddGPU(float *a, 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; // 1M 元素
    size_t size = n * sizeof(float);
    
    // 分配主机内存
    float *h_a = (float *)malloc(size);
    float *h_b = (float *)malloc(size);
    float *h_c = (float *)malloc(size);
    
    // 初始化数据
    for (int i = 0; i < n; i++) {
        h_a[i] = rand() / (float)RAND_MAX;
        h_b[i] = rand() / (float)RAND_MAX;
    }
    
    // CPU 计时
    clock_t start = clock();
    vectorAddCPU(h_a, h_b, h_c, n);
    clock_t end = clock();
    double cpuTime = ((double)(end - start)) / CLOCKS_PER_SEC * 1000;
    printf("CPU time: %.3f ms\n", cpuTime);
    
    // 分配设备内存
    float *d_a, *d_b, *d_c;
    cudaMalloc((void **)&d_a, size);
    cudaMalloc((void **)&d_b, size);
    cudaMalloc((void **)&d_c, size);
    
    // 复制数据到设备
    cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);
    
    // GPU 计时
    cudaEvent_t startEvent, stopEvent;
    cudaEventCreate(&startEvent);
    cudaEventCreate(&stopEvent);
    
    int threadsPerBlock = 256;
    int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
    
    cudaEventRecord(startEvent);
    vectorAddGPU<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, n);
    cudaEventRecord(stopEvent);
    cudaEventSynchronize(stopEvent);
    
    float gpuTime = 0;
    cudaEventElapsedTime(&gpuTime, startEvent, stopEvent);
    printf("GPU kernel time: %.3f ms\n", gpuTime);
    
    // 复制结果回主机
    cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost);
    
    // 清理
    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);
    cudaEventDestroy(startEvent);
    cudaEventDestroy(stopEvent);
    free(h_a);
    free(h_b);
    free(h_c);
    
    return 0;
}

这个示例展示了 CUDA 编程的基本模式:分配设备内存、传输数据、启动内核、取回结果。注意 GPU 内核函数的启动语法 <<<blocksPerGrid, threadsPerBlock>>>,这是 CUDA 特有的语法扩展。

4.2 设备信息查询

cpp
#include <stdio.h>

int main() {
    int deviceCount;
    cudaGetDeviceCount(&deviceCount);
    
    printf("Found %d CUDA-capable device(s)\n\n", deviceCount);
    
    for (int i = 0; i < deviceCount; i++) {
        cudaDeviceProp prop;
        cudaGetDeviceProperties(&prop, i);
        
        printf("Device %d: %s\n", i, prop.name);
        printf("  Compute Capability: %d.%d\n", prop.major, prop.minor);
        printf("  Total Global Memory: %.2f GB\n", 
               prop.totalGlobalMem / (1024.0 * 1024 * 1024));
        printf("  Shared Memory per Block: %.2f KB\n", 
               prop.sharedMemPerBlock / 1024.0);
        printf("  Max Threads per Block: %d\n", prop.maxThreadsPerBlock);
        printf("  Max Threads per Multiprocessor: %d\n", 
               prop.maxThreadsPerMultiProcessor);
        printf("  Multiprocessor Count: %d\n", prop.multiProcessorCount);
        printf("  Memory Clock Rate: %.2f GHz\n", 
               prop.memoryClockRate / 1e6);
        printf("  Memory Bus Width: %d bits\n", prop.memoryBusWidth);
        printf("\n");
    }
    
    return 0;
}

这个程序输出了系统中所有 CUDA 设备的详细信息,包括计算能力、内存大小、线程限制等。这些信息对于优化 CUDA 程序非常重要。

4.3 简单的并行归约

cpp
#include <stdio.h>

// 并行归约内核
__global__ void reduceKernel(float *input, float *output, int n) {
    extern __shared__ float sharedData[];
    
    int tid = threadIdx.x;
    int globalIdx = blockIdx.x * blockDim.x + threadIdx.x;
    
    // 加载数据到共享内存
    if (globalIdx < n) {
        sharedData[tid] = input[globalIdx];
    } else {
        sharedData[tid] = 0.0f;
    }
    __syncthreads();
    
    // 树形归约
    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
        if (tid < stride) {
            sharedData[tid] += sharedData[tid + stride];
        }
        __syncthreads();
    }
    
    // 第一个线程写入结果
    if (tid == 0) {
        output[blockIdx.x] = sharedData[0];
    }
}

int main() {
    int n = 1 << 20;
    size_t size = n * sizeof(float);
    
    float *h_input = (float *)malloc(size);
    for (int i = 0; i < n; i++) {
        h_input[i] = 1.0f; // 期望结果为 n
    }
    
    float *d_input, *d_output;
    cudaMalloc((void **)&d_input, size);
    
    int blocksPerGrid = 256;
    int threadsPerBlock = 256;
    size_t outputSize = blocksPerGrid * sizeof(float);
    cudaMalloc((void **)&d_output, outputSize);
    
    cudaMemcpy(d_input, h_input, size, cudaMemcpyHostToDevice);
    
    int sharedMemSize = threadsPerBlock * sizeof(float);
    reduceKernel<<<blocksPerGrid, threadsPerBlock, sharedMemSize>>>(
        d_input, d_output, n);
    
    float *h_output = (float *)malloc(outputSize);
    cudaMemcpy(h_output, d_output, outputSize, cudaMemcpyDeviceToHost);
    
    float sum = 0.0f;
    for (int i = 0; i < blocksPerGrid; i++) {
        sum += h_output[i];
    }
    
    printf("Sum: %.0f (expected: %d)\n", sum, n);
    
    free(h_input);
    free(h_output);
    cudaFree(d_input);
    cudaFree(d_output);
    
    return 0;
}

这个示例展示了如何使用共享内存和线程同步来实现高效的并行归约。归约是许多并行算法的基础构件。


五、深入思考

5.1 历史背景

GPU 计算的兴起与半导体行业的发展密切相关。在 2000 年代初,CPU 主频的提升遇到了功耗墙和散热限制。与此同时,GPU 制造商为了追求更逼真的图形效果,不断增加并行处理单元的数量。这种架构演变无意中为通用计算创造了条件。

NVIDIA 的决策者敏锐地意识到了这一机会,投入巨资开发 CUDA 平台。这一决策在当时颇具争议,因为这意味着要牺牲一部分图形性能来换取通用计算能力。然而,历史证明这是一个正确的战略选择。

5.2 前沿方向

当前的 GPU 计算研究集中在以下几个方向:

  • 异构计算:更紧密地集成 CPU 和 GPU,减少数据传输开销
  • 统一内存:简化编程模型,让程序员无需手动管理内存传输
  • 张量核心:专门为深度学习优化的硬件单元
  • 光线追踪核心:实时光线追踪的硬件加速
  • 多 GPU 扩展:如何在多个 GPU 之间高效分配工作负载

5.3 延伸阅读

  • "Programming Massively Parallel Processors" by David B. Kirk and Wen-mei W. Hwu
  • NVIDIA CUDA C Programming Guide
  • "GPU Gems" series by NVIDIA
  • IEEE Micro 杂志上的 GPU 架构专题

六、实验笔记

实验 1:测量不同数据规模下的加速比

目标:观察向量加法任务在不同数据规模下 CPU 与 GPU 的性能差异。

步骤:

修改上述向量加法示例中的数据规模 n,分别测试 1K、1M、100M 元素

记录 CPU 时间和 GPU 内核时间

计算加速比(CPU 时间 / GPU 时间)

绘制加速比随数据规模变化的曲线

预期结果:小数据规模时加速比较低(数据传输开销占比大),大数据规模时加速比显著提高。

实验 2:探索不同 GPU 设备的性能差异

目标:比较不同型号 GPU 在同一计算任务上的表现。

步骤:

如果有多个 CUDA 设备,使用 cudaSetDevice() 切换设备

运行相同的基准测试

记录各设备的执行时间

结合设备属性(核心数、内存带宽等)分析性能差异的原因

思考题:为什么高端 GPU 的加速比更高?哪些硬件参数对性能影响最大?


七、本章小结

本章介绍了 GPU 计算的基本概念和 CUDA 平台的背景。我们探讨了 CPU 与 GPU 在架构设计上的根本差异:CPU 优化延迟,GPU 优化吞吐量。我们回顾了 GPU 计算从固定功能管线到可编程着色器,再到 CUDA 通用计算的发展历程。

理解并行计算的分类(SIMD、MIMD 等)有助于我们识别哪些问题适合 GPU 加速。数据并行度高、计算密集、控制流简单的任务最适合 GPU;而控制流复杂、数据依赖性强、计算量小的任务则更适合 CPU。

CUDA 平台的成功得益于其统一的编程模型、丰富的库支持和成熟的生态系统。它使得程序员可以用熟悉的 C/C++ 语言编写 GPU 代码,大大降低了 GPU 编程的门槛。

在后续章节中,我们将深入 CUDA 编程的具体细节,从最简单的 Hello World 程序开始,逐步掌握线程模型、内存管理、同步机制等核心概念。


八、习题

解释 CPU 和 GPU 在设计目标上的根本差异,并举例说明各自适合的应用场景。

什么是 SIMD 和 SIMT?它们之间有什么区别?

根据 Amdahl 定律,如果一个程序有 95% 的代码可以并行化,使用无限多处理器时的最大加速比是多少?

列举三个适合 GPU 加速的应用领域,并解释为什么这些领域适合 GPU。

描述 GPGPU 时代的局限性,以及 CUDA 如何解决这些问题。

为什么内存访问模式对 GPU 性能如此重要?

假设你需要处理一个包含条件分支的复杂算法,你会如何评估它是否适合 GPU 加速?

查阅 NVIDIA 最新一代 GPU 的技术规格,列出其核心数、内存带宽、计算能力等关键参数。

解释 Gustafson 定律与 Amdahl 定律的区别,以及它们对并行计算的不同启示。

如果你要设计一个新的 GPU 架构,针对深度学习工作负载,你会优先考虑哪些优化方向?

1/8为什么选
0%