《CUDA Programming Guide》 2. CUDA C++

§ 参考资料 共 1 条

Basic Usage of CUDA C++

  • 使用 __global__ 来标识一个 Kernel(前面说过入口 Device Function 才叫做 Kernel),__device____host__ 分别标识 Device Function 和 Host Function
  • 使用三重尖括号 kernel_name<<<gridDim, blockDim>>>(...) 来启动 Kernel,其中两个 Dim 可以是数字也可以是 dim3(CUDA 内置类型),表示前面提到的 Grid-Block-Thread 的三级结构
  • 在 Device 函数中使用 gridDimblockDim 来表示 Grid 和 Block 的维度,blockIdxthreadIdx 表示 Block 和 Thread 在 Grid、Block 中的 Index,它们都有 .x.y.z 三个成员
  • Host 函数中,使用 cudaMalloc/cudaFree 来进行申请/释放显存,使用 cudaMallocHost/cudaFreeHost 来申请/释放 Page-Locked 内存(内存页表不会交换,显存内存之间传输数据时会更快),使用 cudaMemcpy 来进行数据传输(内存到显存或者显存到内存),使用 cudaMemset 来初始化显存
  • Host 函数中,也可以使用 cudaMallocManaged 来使用 CUDA 的 Unified Memory 特性,让驱动程序来管理 Host 与 Device 之间的数据移动(注意完成后需要使用 cudaFree 进行释放)
  • Host 函数中,使用 cudaDeviceSynchronize 来阻塞 Host 线程,直到 GPU 上所有先前提交的任务全部完成
  • Device 函数中,使用 __syncthreads() 来进行同一个 Block 内线程的同步
  • Device 函数中,变量也有特定的修饰符用来表示变量存放的位置,__device____constant____shared__ 分别表示变量会存放在 Global Memory、Constant Memory、Shared Memory 中,另外 __managed__ 表示变量是 Unified Memory 的,驱动程序来管理

示例代码:

#include <cuda_runtime_api.h>
#include <memory.h>
#include <cstdlib>
#include <ctime>
#include <stdio.h>
#include <cuda/cmath>

__global__ void vecAdd(float* A, float* B, float* C, int vectorLength) {
  int workIndex = threadIdx.x + blockIdx.x*blockDim.x;
  if(workIndex < vectorLength) {
    C[workIndex] = A[workIndex] + B[workIndex];
  }
}

void initArray(float* A, int length) {
  std::srand(std::time({}));
  for(int i = 0; i < length; i++) {
    A[i] = rand() / (float)RAND_MAX;
  }
}

void serialVecAdd(float* A, float* B, float* C, int length) {
  for(int i = 0; i < length; i++) {
    C[i] = A[i] + B[i];
  }
}

bool vectorApproximatelyEqual(float* A, float* B, int length, float epsilon = 0.00001) {
  for(int i = 0; i < length; i++) {
    if(fabs(A[i] - B[i]) > epsilon) {
      printf("Index %d mismatch: %f != %f", i, A[i], B[i]);
      return false;
    }
  }
  return true;
}

//explicit-memory-begin
void explicitMemExample(int vectorLength) {
  // Pointers for host memory
  float* A = nullptr;
  float* B = nullptr;
  float* C = nullptr;
  float* comparisonResult = (float*)malloc(vectorLength * sizeof(float));
    
  // Pointers for device memory
  float* devA = nullptr;
  float* devB = nullptr;
  float* devC = nullptr;

  // Allocate Host Memory using cudaMallocHost API. This is best practice
  // when buffers will be used for copies between CPU and GPU memory
  cudaMallocHost(&A, vectorLength * sizeof(float));
  cudaMallocHost(&B, vectorLength * sizeof(float));
  cudaMallocHost(&C, vectorLength * sizeof(float));

  // Initialize vectors on the host
  initArray(A, vectorLength);
  initArray(B, vectorLength);

  // start-allocate-and-copy
  // Allocate memory on the GPU
  cudaMalloc(&devA, vectorLength * sizeof(float));
  cudaMalloc(&devB, vectorLength * sizeof(float));
  cudaMalloc(&devC, vectorLength * sizeof(float));

  // Copy data to the GPU
  cudaMemcpy(devA, A, vectorLength * sizeof(float), cudaMemcpyDefault);
  cudaMemcpy(devB, B, vectorLength * sizeof(float), cudaMemcpyDefault);
  cudaMemset(devC, 0, vectorLength * sizeof(float));
  // end-allocate-and-copy

  // Launch the kernel
  int threads = 256;
  int blocks = cuda::ceil_div(vectorLength, threads);
  vecAdd<<<blocks, threads>>>(devA, devB, devC, vectorLength);

  // wait for kernel execution to complete
  cudaDeviceSynchronize();

  // Copy results back to host
  cudaMemcpy(C, devC, vectorLength * sizeof(float), cudaMemcpyDefault);

  // Perform computation serially on CPU for comparison
  serialVecAdd(A, B, comparisonResult, vectorLength);

  // Confirm that CPU and GPU got the same answer
  if(vectorApproximatelyEqual(C, comparisonResult, vectorLength)) {
    printf("Explicit Memory: CPU and GPU answers match\n");
  }
  else {
    printf("Explicit Memory: Error - CPU and GPU answers to not match\n");
  }

  // clean up
  cudaFree(devA);
  cudaFree(devB);
  cudaFree(devC);
  cudaFreeHost(A);
  cudaFreeHost(B);
  cudaFreeHost(C);
  free(comparisonResult);
}
//explicit-memory-end


int main(int argc, char** argv) {
  int vectorLength = 1024;
  if(argc >= 2) {
    vectorLength = std::atoi(argv[1]);
  }
  explicitMemExample(vectorLength);		
  return 0;
}

Writting SIMT Kernels

Memory Spaces

CUDA 设备具有多个可由 CUDA 线程访问的内存空间:

Memory TypeScopeLifetimeLocation
GlobalGridApplicationDevice
ConstantGridApplicationDevice
SharedBlockKernelSM
LocalThreadKernelDevice
RegisterThreadKernelSM
  • Global Memory 是所有 Thread 都可访问的主要设备内存,一般通过 cudaMalloc 来分配,容量大、生命周期长,但是访问延迟通常较高
  • Shared Memory 由 Block 内所有线程共享,容量小但是延迟更低、带宽更高,可以动态或者静态分配
    • 静态分配:Device Function 中,变量写 __shared__ 修饰符,比如:__shared__ float tile[32][32];
    • 动态分配:Device Function 中,写 extern __shared__ shared[];,然后在启动 Kernel 时指定字节数,即 kernel<<<grid, block, sharedBytes>>>();
    • 注意一个 Kernel 内所有 extern __shared__ 声明实际上引用同一块动态 Shared Memory,需要多个数组时,应手动划分并保证地址对齐
    • 静态分配和动态分配可以同时存在
  • Register 是每个 Thread 私有的,它由编译器管理
  • Local Memory 位于 Global Memory 地址空间,但它是 Thread 私有的,由编译器管理。实际上 Local Memory 充当了 CPU 编程中的“栈空间”功能,对于简单的局部变量,GPU 会放在寄存器中,而对于更复杂的变量(例如无法使用常量索引分析的局部数组、较大的局部结构体或数组、寄存器不足时溢出的变量)容易被编译器放进 Local Memory
  • Constant Memory 只能在 Kernel 或者 Device Function 之外声明。适用于存储少量数据,且这些数据需供各线程以只读方式使用。容量较小,通常每个设备仅有 64KB。Host 端可通过运行时库(如 cudaGetSymbolAddress()cudaGetSymbolSize()cudaMemcpyToSymbol()cudaMemcpyFromSymbol())进行访问

Memory Performance

The NVIDIA CUDA Compiler(NVCC)

  • Parallel Thread Execution(PTX):一种面向 GPU 的高级汇编语言,针对某种虚拟 GPU 架构,不同的设备可能只兼容特定版本的 PTX
  • Cubin:针对特定真实 GPU 架构的 SM 版本(例如 sm_120)的二进制格式
  • Fabin:可执行文件(会包含 CPU 代码和 GPU 代码)中的 GPU 代码保存的容器,为了兼容不同的 SM 版本,通常会包含几组 Cubin 二进制和一组或多组 PTX

The NVIDIA CUDA Compiler(NVCC)是用来编译 PTX、CUDA C/C++ 的工具链,属于 CUDA Toolkit 的一部分,包含编译器、链接器以及 PTX 和 Cubin 汇编器等多种工具

File ExtensionDescriptionContent
.cC source fileHost-only code
.cpp, .cc, .cxxC++ source fileHost-only code
.h, .hpp, .hh, .hxxC/C++ header fileDevice code, host code, mix of host/device code
.cuCUDA source fileDevice code, host code, mix of host/device code
.cuhCUDA header fileDevice code, host code, mix of host/device code

NVCC 会区分出来 Host Code 和 Device Code,对于 Host Code 一般来说会调用普通的 C/C++ 编译器(gcc、clang),而对于 Device Code 会调用自己的 GPU 编译器

与普通的编译器相似,GPU Compiler 也会经历“代码 -> 汇编 -> 二进制”的阶段,然后再进行链接

例如对于下面的代码:

// ----- example.cu -----

#include <stdio.h>

__global__ void kernel() {
  printf("Hello from kernel\n");
}

void kernel_launcher() {
  kernel<<<1, 1>>>();
  cudaDeviceSynchronize();
}

int main() {
  kernel_launcher();
  return 0;
}

编译流程大概是这样:

High-level nvcc flow multiple architectures

NVCC 的 GPU 编译器也支持分离编译,但是它默认并不开启,即遵循 whole-program compilation,假定当前编译单元中能够找到其需要的全部设备代码,需要手动开启。同样的,为了弥补分离编译带来的跨文件优化损失,也可以使用链接时优化 LTO

NVCC 默认会压缩可执行文件或库中的 Fatbin,可以通过选项调整压缩策略(speedsizebalancenone

Unified and System Memory