CUDA(Compute Unified Device Architecture)是NVIDIA推出的并行计算平台和编程模型,允许开发者利用GPU的众核架构进行通用计算。以下是一份面向大厂面试的CUDA编程详解,涵盖核心概念、优化策略和常见考点。

🚀 CUDA编程详解与面试指南

1️⃣ CUDA核心概念与架构

1.1 基本架构

CUDA采用主机-设备模型(Host-Device),CPU作为主机处理控制逻辑,GPU作为设备执行数据并行计算。GPU由多个流式多处理器(Streaming Multiprocessors, SM)组成,每个SM包含多个CUDA核心,可并发执行大量线程。

1.2 线程组织模型

CUDA的线程组织采用分层结构:

  • 线程(Thread):最小执行单元,每个线程执行核函数的一个实例。
  • 线程块(Block):一组线程,可共享共享内存(Shared Memory),块内线程可通过__syncthreads()同步。
  • 网格(Grid):多个线程块的集合,执行同一核函数。

线程索引计算:

int idx = blockIdx.x * blockDim.x + threadIdx.x;  // 一维网格与块

三维索引可扩展至多维数据处理(如图像处理)。

1.3 内存层次

CUDA提供多级内存,各有特点:

内存类型作用域延迟/速度容量
寄存器单个线程最快少量
共享内存线程块内非常快较小(KB级)
全局内存所有线程+主机慢(高延迟)大(GB级)
常量内存所有线程(只读)中等(有缓存)较小
纹理内存所有线程(只读)中等(优化2D访问)较小

2️⃣ CUDA编程基础

2.1 核函数(Kernel)

核函数使用__global__修饰符定义,在GPU上执行:

__global__ void vectorAdd(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];
}

调用核函数需指定执行配置(网格/块维度):

dim3 blocks(256);      // 每块256线程
dim3 grids((N + 255) / 256); // 网格大小覆盖所有数据
vectorAdd<<<grids, blocks>>>(A, B, C, N);

2.2 内存管理

GPU内存需通过CUDA API管理:

float *d_A, *h_A;
h_A = (float*)malloc(N * sizeof(float)); // 主机内存
cudaMalloc(&d_A, N * sizeof(float));     // 设备内存
cudaMemcpy(d_A, h_A, N * sizeof(float), cudaMemcpyHostToDevice); // 主机到设备
cudaMemcpy(h_A, d_A, N * sizeof(float), cudaMemcpyDeviceToHost); // 设备到主机
cudaFree(d_A); // 释放设备内存

3️⃣ 性能优化策略(面试重点)

3.1 内存访问优化

  • 合并访问(Coalesced Access):确保相邻线程访问全局内存中相邻地址,合并为单个内存事务。非合并访问会导致带宽浪费。
  • 共享内存缓存:减少全局内存访问,将频繁访问的数据缓存至共享内存。尤其适用于矩阵乘法、归约等操作。
  • 使用常量/纹理内存:只读数据放入常量内存或纹理内存,利用缓存加速访问。

3.2 执行配置优化

  • 块大小(Block Size):通常设置为32的倍数(如128、256、512),以匹配Warp大小(32线程)。
  • 网格大小(Grid Size):覆盖所有数据,避免线程不足或浪费。
  • 占用率(Occupancy):通过调整块大小和共享内存使用,提高SM的线程占用率。

3.3 向量化访存

使用float4、int2等向量类型一次读写多个数据,减少指令数,提高带宽利用率:

__global__ void add_float4(float* a, float* b, float* c, int N) {
    int idx = (blockDim.x * blockIdx.x + threadIdx.x) * 4;
    if (idx < N) {
        float4 va = *(float4*)(a + idx);
        float4 vb = *(float4*)(b + idx);
        float4 vc;
        vc.x = va.x + vb.x;
        vc.y = va.y + vb.y;
        vc.z = va.z + vb.z;
        vc.w = va.w + vb.w;
        *(float4*)(c + idx) = vc;
    }
}

调用时网格大小需除以4:gridSize = CEIL(N/4, blockSize)。

4️⃣ 高频面试算子与优化

4.1 归约(Reduction)

问题:对数组求和/求最大值等操作。
优化点:

  • 避免原子操作:原子操作(如atomicAdd)导致串行化,性能低下。
  • 分层归约:在块内使用共享内存进行归约,再跨块合并。
  • Warp级优化:利用Warp内线程的隐式同步,使用__shfl_down_sync等指令减少同步开销。

示例代码(Warp Shuffle归约):

__global__ void reduce_sum(float* d_in, float* d_out, int N) {
    __shared__ float sdata[32];
    int idx = blockDim.x * blockIdx.x + threadIdx.x;
    int warpId = threadIdx.x / warpSize;
    int laneId = threadIdx.x % warpSize;
    float val = (idx < N) ? d_in[idx] : 0.0f;
    // Warp内归约
    for (int offset = warpSize/2; offset > 0; offset >>= 1) 
        val += __shfl_down_sync(0xFFFFFFFF, val, offset);
    if (laneId == 0) sdata[warpId] = val; // Warp结果存共享内存
    __syncthreads();
    // 第一个Warp归约所有Warp的结果
    if (warpId == 0) {
        float val2 = (laneId < blockDim.x/warpSize) ? sdata[laneId] : 0;
        for (int offset = warpSize/2; offset > 0; offset >>= 1) 
            val2 += __shfl_down_sync(0xFFFFFFFF, val2, offset);
        if (laneId == 0) atomicAdd(d_out, val2);
    }
}

4.2 矩阵乘法(GEMM)

问题:计算( C = A \times B ),其中( A \in \mathbb{R}^{m \times k} ), ( B \in \mathbb{R}^{k \times n} )。
优化点:

  • 分块(Tiling):将矩阵分块加载到共享内存,减少全局内存访问。
  • 双缓冲:重叠计算与数据加载,隐藏内存延迟。
  • 使用Tensor Core:FP16矩阵乘积累加运算(HMMA),极大提升吞吐量(需Volta及以上架构)。

4.3 矩阵转置

问题:对矩阵进行转置操作。
优化点:

  • 共享内存避免Bank冲突:按行加载,按列存储时使用共享内存中转,并调整内存布局避免Bank冲突。
  • 向量化访存:使用float4等类型提高带宽利用率。

5️⃣ 编程技巧与面试注意

5.1 同步机制

  • 块内同步:__syncthreads()同步块内所有线程。
  • Warp级同步:Warp内线程自然同步,无需显式同步指令。
  • 原子操作:跨块同步需使用原子操作(如atomicAdd),但需谨慎使用以免性能下降。

5.2 错误处理

CUDA API和核函数调用可能出错,需检查返回值:

cudaError_t err = cudaGetLastError();
if (err != cudaSuccess) 
    printf("Error: %s\n", cudaGetErrorString(err));

使用cudaDeviceSynchronize()确保核函数执行完毕再检查错误。

5.3 调试与性能分析工具

  • NVCC:CUDA编译器。
  • NSight Systems:性能分析工具,分析内核执行、内存传输等。
  • CUDA-MEMCHECK:内存错误检查工具。

6️⃣ 面试常见问题

  1. 全局内存合并访问条件?
    相邻线程访问相邻地址,且地址对齐(如128字节对齐)。

  2. 共享内存Bank冲突?如何避免?
    Bank冲突:多个线程同时访问同一Bank的不同地址导致串行化。
    避免方法:调整数据布局或访问步长(如使用填充),确保同一Bank内各线程访问不同地址。

  3. Warp发散(Warp Divergence)?如何避免?
    同一Warp内线程执行不同分支导致串行化。
    避免方法:尽量确保同一Warp内线程走相同分支;重构算法或数据布局减少发散。

  4. CUDA流(Streams)的作用?
    实现并行:并发内核执行、重叠计算与数据传输。

  5. 如何设置块大小?
    考虑共享内存/寄存器使用、Warp占用率,通常通过实验选取最佳值。

💎 总结

掌握CUDA编程需理解其并行架构、内存层次和优化技巧。面试时重点展示对性能优化的理解,如内存访问模式、归约、矩阵操作优化等。结合项目经验讨论实际应用(如深度学习、科学计算)会更具竞争力。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐