GitHub Repo 骨架:Makefile + CUDA 入门程序
这是一个为 CUDA 项目设计的 GitHub repo 骨架,它提供了一个清晰、简洁的起始点,特别适合 CUDA 初学者或希望拥有一个标准化项目结构的开发者。
仓库结构
项目将遵循以下文件和目录结构:
css
.
├── Makefile
├── README.md
├── inc
│ └── utils.cuh
├── src
│ ├── basic_vector_add.cu
│ └── starter_kernel.cu
└── data
└── sample_input.txt
文件说明
Makefile
这个 Makefile 自动化了 CUDA 程序的编译过程。它定义了编译器 (NVCC
)、头文件路径和源文件等变量,让你可以通过简单的 make
命令来构建项目。
Makefile
makefile
# Makefile for CUDA projects
# 编译器和编译选项
NVCC = nvcc
NVCC_FLAGS = -g -G -std=c++17
# 目录
INC_DIR = ./inc
SRC_DIR = ./src
BUILD_DIR = ./bin
# 源文件
SRC_FILES = $(wildcard $(SRC_DIR)/*.cu)
# 可执行文件
EXECUTABLES = $(patsubst $(SRC_DIR)/%.cu, $(BUILD_DIR)/%, $(SRC_FILES))
# 所有目标
.PHONY: all
all: $(EXECUTABLES)
@echo "所有 CUDA 程序编译成功。"
# 编译每个源文件的规则
$(BUILD_DIR)/%: $(SRC_DIR)/%.cu
@mkdir -p $(BUILD_DIR)
$(NVCC) $(NVCC_FLAGS) -o $@ $< -I$(INC_DIR)
@echo "编译 $< 为 $@"
# 清理目标
.PHONY: clean
clean:
@rm -rf $(BUILD_DIR)
@echo "已清理构建目录。"
inc/utils.cuh
这个头文件包含了一些实用的 CUDA 编程工具,例如用于错误检查的宏。将这些通用功能放在一个专门的头文件中,可以提高代码的复用性和清晰度。
C++
arduino
#ifndef UTILS_CUH
#define UTILS_CUH
#include <iostream>
#include <cuda_runtime.h>
#define CHECK(call) \
{ \
const cudaError_t error = call; \
if (error != cudaSuccess) \
{ \
fprintf(stderr, "错误: %s:%d, ", __FILE__, __LINE__); \
fprintf(stderr, "错误码: %d, 原因: %s\n", error, \
cudaGetErrorString(error)); \
exit(1); \
} \
}
#endif // UTILS_CUH
入门级 CUDA 程序
src/basic_vector_add.cu
这是并行编程中最经典的"Hello, World!"。该程序通过在 GPU 上对两个大型向量进行相加,展示了数据并行性的核心概念。这是一个非常好的、值得运行和理解的第一个例子。
C++
scss
#include <iostream>
#include "utils.cuh"
// 核函数:向量相加
__global__ void vectorAdd(const float* A, const float* B, float* C, int numElements) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < numElements) {
C[i] = A[i] + B[i];
}
}
int main() {
int numElements = 50000;
size_t size = numElements * sizeof(float);
float *h_A, *h_B, *h_C; // 主机端向量
float *d_A, *d_B, *d_C; // 设备端向量
// 分配主机内存
h_A = (float*)malloc(size);
h_B = (float*)malloc(size);
h_C = (float*)malloc(size);
// 初始化主机向量
for (int i = 0; i < numElements; ++i) {
h_A[i] = 1.0f;
h_B[i] = 2.0f;
}
// 分配设备内存并复制主机数据
CHECK(cudaMalloc((void**)&d_A, size));
CHECK(cudaMalloc((void**)&d_B, size));
CHECK(cudaMalloc((void**)&d_C, size));
CHECK(cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice));
CHECK(cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice));
// 定义网格和块的维度
int threadsPerBlock = 256;
int blocksPerGrid = (numElements + threadsPerBlock - 1) / threadsPerBlock;
// 启动核函数
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, numElements);
CHECK(cudaGetLastError());
// 将结果复制回主机
CHECK(cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost));
// 验证结果
float maxError = 0.0f;
for (int i = 0; i < numElements; ++i) {
maxError = fmax(maxError, abs(h_C[i] - 3.0f));
}
std::cout << "最大误差: " << maxError << std::endl;
if (maxError > 1e-5) {
std::cout << "验证失败。" << std::endl;
} else {
std::cout << "验证成功!" << std::endl;
}
// 释放内存
free(h_A);
free(h_B);
free(h_C);
CHECK(cudaFree(d_A));
CHECK(cudaFree(d_B));
CHECK(cudaFree(d_C));
return 0;
}
src/starter_kernel.cu
一个最基础的例子,展示了核函数启动语法以及线程是如何在网格(grid)和块(block)的层次结构中组织的。它简单地打印出线程和块的索引。
C++
arduino
#include <iostream>
#include "utils.cuh"
// 打印线程和块ID的核函数
__global__ void starterKernel() {
int tid = threadIdx.x;
int bid = blockIdx.x;
printf("来自块 %d 的线程 %d,你好\n", bid, tid);
}
int main() {
// 定义一个二维的块网格,每个块包含二维的线程网格
dim3 gridDim(2); // 2 个块
dim3 blockDim(4); // 每个块 4 个线程
// 用定义的维度启动核函数
starterKernel<<<gridDim, blockDim>>>();
// 同步以确保所有核函数打印完成后程序才退出
CHECK(cudaDeviceSynchronize());
return 0;
}
编译程序
直接在根目录下运行make编译
bash
$ make
nvcc -g -G -std=c++17 -o bin/basic_vector_add src/basic_vector_add.cu -I./inc
编译 src/basic_vector_add.cu 为 bin/basic_vector_add
nvcc -g -G -std=c++17 -o bin/starter_kernel src/starter_kernel.cu -I./inc
编译 src/starter_kernel.cu 为 bin/starter_kernel
所有 CUDA 程序编译成功。
运行
bash
./bin/basic_vector_add
最大误差: 0
验证成功!
./bin/starter_kernel
来自块 1 的线程 0,你好
来自块 1 的线程 1,你好
来自块 1 的线程 2,你好
来自块 1 的线程 3,你好
来自块 0 的线程 0,你好
来自块 0 的线程 1,你好
来自块 0 的线程 2,你好
来自块 0 的线程 3,你好