cuda-samples 官方 Template 示例全解:从零搭建 CUDA 工程并掌握设备内存分配
2026/9/16 15:54:21 网站建设 项目流程

cuda-samples 官方 Template 示例全解:从零搭建 CUDA 工程并掌握设备内存分配

【免费下载链接】cuda-samplesSamples for CUDA Developers which demonstrates features in CUDA Toolkit项目地址: https://gitcode.com/GitHub_Trending/cu/cuda-samples

本篇技术指南以 NVIDIA cuda-samples 仓库中cpp/0_Introduction/template的 README.md 为骨架,结合其 template.cu、template_cpu.cpp 与 CMakeLists.txt 源码,完整讲解这个“模板工程”的定位、支持的软硬件范围、核心 CUDA Runtime API 用法(cudaMalloc / cudaMemcpy / cudaFree)、内核与宿主端代码的每一处细节,以及基于 CMake 的构建与运行方式。读完本文,你将能够把该模板作为起点,快速搭建自己的 CUDA 项目骨架,并理解设备内存分配这一贯穿所有 CUDA 程序的基础概念。

一、模板工程是什么

按仓库描述,template是一个trivial template project(极简模板工程),用途是作为创建新 CUDA 项目的起点。它位于 cuda-samples 的入门分类0_Introduction下(参见 cpp/0_Introduction/CMakeLists.txt 中第 41 行的add_subdirectory(template)),并在顶层 CMakeLists.txt 中通过add_subdirectory(cpp)被纳入整体构建。

它演示的核心技术概念是Device Memory Allocation(设备内存分配),即宿主机(Host)与设备(Device)之间内存的申请、拷贝与释放流程。整个目录只有 4 个文件,结构非常干净:

文件作用
README.md工程说明:用途、支持范围、涉及 API
template.cu宿主端主程序 + 一个简单的 CUDA 测试内核
template_cpu.cppCPU 参考实现(黄金数据生成)
CMakeLists.txtCMake 构建脚本

从源码结构看,该模板刻意保持“小而全”:既有设备端内核,又有宿主端内存管理、计时、结果校验与回归测试输出,非常适合作为新项目的脚手架对照学习。

二、支持范围与前置条件

原 README 明确给出了该模板(以及绝大多数 cuda-samples)的软硬件支持范围:

  • 支持的 SM 架构:SM 5.0、5.2、5.3、6.0、6.1、7.0、7.2、7.5、8.0、8.6、8.7、8.9、9.0。对应到实际 GPU,即 Maxwell 到 Hopper/Blackwell 一代的计算能力。
  • 支持的操作系统:Linux、Windows。
  • 支持的 CPU 架构:x86_64、armv7l。
  • 涉及的关键 CUDA Runtime APIcudaMalloccudaMemcpycudaFree
  • 前置条件:为对应平台下载并安装 CUDA Toolkit。此外,由于工程使用 CMake 构建,环境中还需具备满足 CMakeLists.txt 要求的 CMake(cmake_minimum_required(VERSION 3.20))以及支持 C++17 / CUDA 17 标准的编译器工具链。

说明:README 中列出的 SM 支持范围是 NVIDIA 官方在文档层面的声明;而实际构建时具体编译到哪些架构,由构建脚本中的CMAKE_CUDA_ARCHITECTURES决定,下文第四节会展开。

三、设备内存分配:三个核心 API 的模板级示范

README 明确点出本模板的关键 API 是 CUDA Runtime API 中的cudaMalloccudaMemcpycudaFree。它们在 template.cu 中的用法如下:

// 为输入数据分配设备内存 float *d_idata; checkCudaErrors(cudaMalloc((void **)&d_idata, mem_size)); // 将宿主机内存拷贝到设备 checkCudaErrors(cudaMemcpy(d_idata, h_idata, mem_size, cudaMemcpyHostToDevice)); // 为输出数据分配设备内存 float *d_odata; checkCudaErrors(cudaMalloc((void **)&d_odata, mem_size)); // ...执行内核... // 将结果从设备拷回宿主机 checkCudaErrors(cudaMemcpy(h_odata, d_odata, sizeof(float) * num_threads, cudaMemcpyDeviceToHost)); // 释放设备内存 checkCudaErrors(cudaFree(d_idata)); checkCudaErrors(cudaFree(d_odata));

三个 API 的职责与参数要点:

  • cudaMalloc(void** devPtr, size_t size):在设备全局内存中申请size字节,把地址写入devPtr。注意第一个参数是指向指针的指针,因为需要在宿主端拿到设备端地址。与malloc类似,cudaMalloc成功时返回cudaSuccess
  • cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind):在宿主与设备(或设备与设备)之间拷贝count字节。本模板用到两种方向:cudaMemcpyHostToDevice(入参下发)与cudaMemcpyDeviceToHost(结果回传)。
  • cudaFree(void* devPtr):释放由cudaMalloc申请的设备内存,与free对应。

值得注意的工程实践是:模板中的每个 API 调用都被checkCudaErrors宏包裹。该宏定义在 Common/helper_cuda.h 第 598 行:

#define checkCudaErrors(val) check((val), #val, __FILE__, __LINE__)

它会把返回值转成错误字符串,并带上文件名与行号输出到 stderr,出错即exit(EXIT_FAILURE)(参见 helper_cuda.h 的check实现)。这是所有 cuda-samples 统一采用的错误检查范式,也是你写新 CUDA 工程时应复刻的习惯:每个 CUDA API 调用都必须检查返回值

四、主程序逐段解析:template.cu

template.cu 是本模板的主文件,包含设备内核与宿主端完整流程。文件头注释明确说明它是 “Template project which demonstrates the basics on how to setup a project example application. Host code.”

4.1 头文件组织

// includes, system #include <math.h> #include <stdio.h> #include <stdlib.h> #include <string.h> // includes CUDA #include <cuda_runtime.h> // includes, project #include <helper_cuda.h> #include <helper_functions.h> // helper functions for SDK examples
  • cuda_runtime.h:CUDA Runtime API 入口头文件,提供cudaMalloc等运行时函数。
  • helper_cuda.h:cuda-samples 公共辅助头文件(位于 Common/helper_cuda.h),提供checkCudaErrorsgetLastCudaErrorfindCudaDevice等宏与函数。
  • helper_functions.h:SDK 辅助函数(计时器、数据比较、文件输出等),位于 Common/helper_functions.h。

这种“系统头文件 / CUDA 头文件 / 项目辅助头文件”三段式分组是模板示范的代码组织规范。

4.2 设备端测试内核 testKernel

__global__ void testKernel(float *g_idata, float *g_odata) { // shared memory // the size is determined by the host application extern __shared__ float sdata[]; // access thread id const unsigned int tid = threadIdx.x; // access number of threads in this block const unsigned int num_threads = blockDim.x; // read in input data from global memory sdata[tid] = g_idata[tid]; __syncthreads(); // perform some computations sdata[tid] = (float)num_threads * sdata[tid]; __syncthreads(); // write data to global memory g_odata[tid] = sdata[tid]; }

该内核(template.cu)虽然简单,却浓缩了 CUDA 编程的多个核心要素:

  1. extern __shared__ float sdata[]:动态共享内存声明,实际大小由宿主端在内核启动参数中指定(下文 4.4 节会看到mem_size作为第三个启动参数传入)。
  2. 线程标识threadIdx.x是当前线程在块内的索引,blockDim.x是本块的线程数,二者共同定位线程。
  3. 全局→共享内存的搬运:先把g_idata[tid]读入共享内存,体现“全局内存慢、共享内存快”的经典优化思想。
  4. 两次__syncthreads():块内屏障同步,确保所有线程完成共享内存写入后再读取,避免数据竞争——这是共享内存使用中必须遵守的纪律。
  5. 计算:每个元素乘以块内线程数(num_threads),这是一个刻意设计成“可由 CPU 轻松复算”的变换,方便结果校验。

4.3 主函数入口

int main(int argc, char **argv) { runTest(argc, argv); }

main只做一件事:把命令行参数转交给runTest。整体业务逻辑全部封装在runTest中,这种结构便于扩展成更复杂的测试框架。

4.4 runTest:完整的宿主端流程

template.cu 的runTest演示了 CUDA 程序的完整生命周期,按阶段拆解:

① 选择设备

int devID = findCudaDevice(argc, (const char **)argv);

findCudaDevice定义在 Common/helper_cuda.h:若命令行指定了-device=N,则使用该设备;否则自动挑选“Gflops/s 最高”的设备(gpuGetMaxGflopsDeviceId)并打印其计算能力(compute capability)。这解决了多 GPU 机器上的设备选择问题。

② 计时器

StopWatchInterface *timer = 0; sdkCreateTimer(&timer); sdkStartTimer(&timer); // ... 核心计算 ... sdkStopTimer(&timer); printf("Processing time: %f (ms)\n", sdkGetTimerValue(&timer)); sdkDeleteTimer(&timer);

使用helper_functions.h提供的 SDK 计时器测量包括内存拷贝与内核执行在内的总耗时,输出单位为毫秒。

③ 内存分配与初始化

unsigned int num_threads = 32; unsigned int mem_size = sizeof(float) * num_threads; float *h_idata = (float *)malloc(mem_size); for (unsigned int i = 0; i < num_threads; ++i) { h_idata[i] = (float)i; }

模板默认创建 32 个线程、32 个 float 元素,宿主输入数据初始化为0.0f, 1.0f, ..., 31.0f

④ 设备内存分配与数据下发

float *d_idata; checkCudaErrors(cudaMalloc((void **)&d_idata, mem_size)); checkCudaErrors(cudaMemcpy(d_idata, h_idata, mem_size, cudaMemcpyHostToDevice)); float *d_odata; checkCudaErrors(cudaMalloc((void **)&d_odata, mem_size));

即第三节讲解的“设备内存分配”三 API 的实际落地:输入数据上载、输出缓冲预留。

⑤ 内核启动配置与执行

dim3 grid(1, 1, 1); dim3 threads(num_threads, 1, 1); testKernel<<<grid, threads, mem_size>>>(d_idata, d_odata); getLastCudaError("Kernel execution failed");
  • 网格为1×1×1单块,块内 32 个线程,第三个启动参数mem_size就是内核里extern __shared__ float sdata[]的动态共享内存大小(32 个 float = 128 字节)。
  • 启动后立即调用getLastCudaError检查内核是否产生异步错误。该宏定义于 Common/helper_cuda.h,内部通过cudaGetLastError()捕获错误并打印文件、行号与错误描述后退出(helper_cuda.h)。这是捕获内核启动异步错误的推荐做法。

⑥ 结果回传与校验

float *h_odata = (float *)malloc(mem_size); checkCudaErrors(cudaMemcpy(h_odata, d_odata, sizeof(float) * num_threads, cudaMemcpyDeviceToHost));

回传后,模板提供两种结果处理路径(由命令行标志-regression区分):

  • 回归测试模式checkCmdLineFlag(argc, argv, "regression")):调用sdkWriteFile("./data/regression.dat", h_odata, num_threads, 0.0f, false)把输出写入文件,供外部回归比对脚本使用。
  • 普通模式:调用compareData(reference, h_odata, num_threads, 0.0f, 0.0f),把 GPU 结果与 CPU 参考结果逐元素比对,通过则返回 true。

⑦ 清理与退出

free(h_idata); free(h_odata); free(reference); checkCudaErrors(cudaFree(d_idata)); checkCudaErrors(cudaFree(d_odata)); exit(bTestResult ? EXIT_SUCCESS : EXIT_FAILURE);

宿主内存用free、设备内存用cudaFree,一一对应,最后以测试结果决定进程退出码——这是可被脚本判断成败的规范做法。

五、CPU 参考实现:template_cpu.cpp

template_cpu.cpp 提供黄金参考(golden reference)函数computeGold

extern "C" void computeGold(float *reference, float *idata, const unsigned int len); void computeGold(float *reference, float *idata, const unsigned int len) { const float f_len = static_cast<float>(len); for (unsigned int i = 0; i < len; ++i) { reference[i] = idata[i] * f_len; } }

它用纯 CPU 循环复现内核的变换:output[i] = input[i] * len(内核中是乘以块内线程数,二者在单块场景下等价,均为 32)。注意该文件通过extern "C"导出 C 接口,确保在混合编译(.cu + .cpp)时符号链接正确。“GPU 结果 vs CPU 黄金结果”的双轨校验是 cuda-samples 的标准验证模式,也是你移植该模板时应当保留的骨架。

六、CMake 构建脚本解析

template 的 CMakeLists.txt 展示了 cuda-samples 的统一构建范式:

cmake_minimum_required(VERSION 3.20) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../../cmake/Modules") project(template LANGUAGES C CXX CUDA) find_package(CUDAToolkit REQUIRED) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_CUDA_ARCHITECTURES 75 80 86 87 89 90 100 110 120) set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Wno-deprecated-gpu-targets") if(ENABLE_CUDA_DEBUG) set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -G") else() set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -lineinfo") endif() include_directories(../../../Common) add_executable(template template.cu template_cpu.cpp) target_compile_options(template PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:--extended-lambda>) target_compile_features(template PRIVATE cxx_std_17 cuda_std_17) set_target_properties(template PROPERTIES CUDA_SEPARABLE_COMPILATION ON) include(${CMAKE_CURRENT_SOURCE_DIR}/../../../cmake/InstallSamples.cmake) setup_samples_install()

逐项解读要点:

  • 语言启用project(template LANGUAGES C CXX CUDA)find_package(CUDAToolkit REQUIRED)是 CUDA 工程的标配,后者由 CMake 3.20+ 原生支持。
  • 目标架构列表CMAKE_CUDA_ARCHITECTURES 75 80 86 87 89 90 100 110 120,覆盖 Turing(7.5) 到 Blackwell 之后的主流架构,与顶层 CMakeLists.txt 第 15 行的设置一致。它决定编译出的 cubin/PTX 目标,可替换为当前机器 GPU 对应的架构以缩短编译时间。
  • 调试支持-G生成 cuda-gdb 可用的调试信息(会显著影响性能);默认构建则加-lineinfo注入行号信息,供性能剖析工具(如 Nsight Compute)使用。二者通过ENABLE_CUDA_DEBUG开关互斥。
  • 公共头文件include_directories(../../../Common)指向仓库公共辅助目录(Common),因此源码才能#include <helper_cuda.h>
  • 混合编译add_executable(template template.cu template_cpu.cpp)把 CUDA 源与 C++ 源编入同一可执行文件;cxx_std_17 cuda_std_17统一语言标准;CUDA_SEPARABLE_COMPILATION ON开启可分离编译,是 CUDA 17 标准下管理设备代码的现代做法。
  • 安装规则:通过cmake/InstallSamples.cmakesetup_samples_install()统一处理安装逻辑(该脚本位于 cmake/InstallSamples.cmake)。

七、构建与运行

由于仓库是只读的,你应先把 cuda-samples 克隆到本地再进行构建。推荐从仓库根目录用 CMake 构建整个工程,或仅构建 template 目标:

# 1. 克隆仓库(仅在需要时) git clone https://gitcode.com/GitHub_Trending/cu/cuda-samples # 2. 配置并构建(仓库根目录) mkdir build && cd build cmake .. cmake --build . --target template -j # 3. 运行(模板默认使用 Gflops/s 最高的设备) ./0_Introduction/template/template # 4. 指定设备运行 ./0_Introduction/template/template -device=0 # 5. 回归模式运行(将结果写入 ./data/regression.dat) ./0_Introduction/template/template -regression

程序运行时会打印启动信息、所选 GPU 设备与计算能力(例如GPU Device 0: "..." with compute capability 8.9)、内核与内存拷贝的总处理时间(毫秒),最后依据结果比对输出退出码。

命令行参数小结(均由宿主代码支持):

参数行为
-device=N使用编号为 N 的 CUDA 设备(由findCudaDevice解析,见 helper_cuda.h)
-regression把输出写入./data/regression.dat,供回归测试使用(见 template.cu)

构建前提:已安装与当前平台匹配的 CUDA Toolkit(满足 README 的前置条件),CMake ≥ 3.20,以及支持 C++17/CUDA17 的编译器。注意 README 声明的支持范围是 Linux、Windows 上的 x86_64 与 armv7l 架构。

八、从模板到新项目的改造建议

基于对模板四个文件的源码级梳理,可以总结出一套“模板 → 新项目”的改造路线:

  1. 复制目录:复制cpp/0_Introduction/template/整个目录,重命名为你的项目名(如myKernel)。
  2. 改 CMakeLists:把project(template ...)add_executable(template ...)中的template换成新名称;如需新源文件,追加到add_executable列表。
  3. 替换内核:仿照testKernel的结构重写设备端函数(保持“全局内存读入 → 共享内存 →__syncthreads()→ 计算 → 写回”的骨架),同步修改网格/块配置与动态共享内存大小。
  4. 替换参考实现:在computeGold中实现与新内核等价的 CPU 逻辑,compareData会自动完成 GPU/CPU 结果比对。
  5. 注册到构建树:在 cpp/0_Introduction/CMakeLists.txt 中仿照其他子目录添加add_subdirectory(新目录名),或在需要时改为独立构建。

这个模板的价值正在于此:它把“设备内存分配 + 内核启动 + 错误检查 + 结果校验 + 回归输出”这些每个 CUDA 程序都需要的样板一次性配齐,你只需在固定位置上填入自己的业务逻辑,即可快速获得一个结构规范、可验证、可回归的 CUDA 工程起点。

【免费下载链接】cuda-samplesSamples for CUDA Developers which demonstrates features in CUDA Toolkit项目地址: https://gitcode.com/GitHub_Trending/cu/cuda-samples

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询