《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 函数中使用
gridDim、blockDim来表示 Grid 和 Block 的维度,blockIdx、threadIdx表示 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 Type | Scope | Lifetime | Location |
|---|---|---|---|
| Global | Grid | Application | Device |
| Constant | Grid | Application | Device |
| Shared | Block | Kernel | SM |
| Local | Thread | Kernel | Device |
| Register | Thread | Kernel | SM |
- 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,需要多个数组时,应手动划分并保证地址对齐 - 静态分配和动态分配可以同时存在
- 静态分配:Device Function 中,变量写
- 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 Extension | Description | Content |
|---|---|---|
.c | C source file | Host-only code |
.cpp, .cc, .cxx | C++ source file | Host-only code |
.h, .hpp, .hh, .hxx | C/C++ header file | Device code, host code, mix of host/device code |
.cu | CUDA source file | Device code, host code, mix of host/device code |
.cuh | CUDA header file | Device 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;
}
编译流程大概是这样:

NVCC 的 GPU 编译器也支持分离编译,但是它默认并不开启,即遵循 whole-program compilation,假定当前编译单元中能够找到其需要的全部设备代码,需要手动开启。同样的,为了弥补分离编译带来的跨文件优化损失,也可以使用链接时优化 LTO
NVCC 默认会压缩可执行文件或库中的 Fatbin,可以通过选项调整压缩策略(speed、size、balance、none)