CUDA C编程权威指南-第十章:程序实现的注意事项
CUDA C编程权威指南-第十章:程序实现的注意事项
系列导航:导读 | 上一篇:第9章 多GPU编程
系列最后一篇(第 10 章)。前面九章把编程模型、执行模型、内存、流、库、多 GPU 都过了一遍;这一章回到工程实践:怎么把这些串成可重复的开发与调优流程、怎么调试、怎么从 C 移植到 CUDA,在实际项目里少踩坑、写出既对又快的程序。
前言:从学到用
前九章依次覆盖了 CUDA 的编程模型、执行模型、内存层次、共享内存与常量内存、流与并发、指令级原语、GPU 加速库与 OpenACC、多 GPU 编程。这些构成了「怎么写、怎么跑、怎么优化」的完整知识链。但在真实项目中,除了算法与 API,还需要:一套可重复的开发与调优流程、可靠的调试手段,以及从现有 C 代码迁移到 CUDA 的实战经验。
这一章会说明指出:第 10 章「程序实现的注意事项」不引入新的核心 API,而是将前面各章的知识串成工程实践。你将系统学到:开发流程(需求→热点→并行化策略→实现与验证→调优→部署)、剖析驱动的优化(nvprof/Nsight、关键指标、迭代流程)、CUDA 程序的调试(主机/设备端错误处理、cuda-memcheck、Nsight、printf 与单线程调试),以及从 C 到 CUDA 的移植(识别热点、数据与线程划分、常见陷阱与最佳实践)。通过本章,你将对「如何在实际项目中实现与交付 CUDA 程序」形成清晰图景,并为后续进阶打好工程基础。
一、本章在全书中的位置与学习目标
1.1 为什么要学「程序实现的注意事项」
前几章我们主要在单 GPU 上学习怎么写核函数、怎么组织线程与内存、怎么用流与库、怎么扩展到多 GPU。需要留意的是:在实际项目中,规范的流程能减少返工、便于团队协作,并确保性能调优有的放矢;不检查 API 返回值、核函数后不同步就查错等习惯性疏漏,会导致难以定位的崩溃或错误结果;一上来就追求性能而忽略正确性,往往事倍功半。因此,本章回答的是:如何从「会写 CUDA 代码」进阶到「能在真实项目中正确实现、调试、调优并交付」。
学完本章,你应能建立「评估→并行化→优化→部署」的闭环思维,会用剖析工具定位瓶颈并解读关键指标,能系统地进行主机端与设备端错误检查与调试,并掌握从 C 到 CUDA 移植的典型步骤与常见陷阱,避免「仅用单块、忽略错误、未做边界检查」等致命错误。
1.2 学完本章,你应该能回答
| 学习目标 | 检验方式 |
|---|---|
| 理解 CUDA C 开发流程 各阶段及产出 | 能说出需求与热点分析→并行化策略→实现与验证→性能调优→集成部署,并说明「能库则库、正确性优先」 |
| 掌握 APOD 循环的含义与用法 | 能解释 Assess / Parallelize / Optimize / Deploy 各步,并说明为何要「用剖析数据驱动优化对象」 |
| 会用 nvprof / Nsight 做快速剖析并解读关键指标 | 能说出 achieved_occupancy、gld_efficiency、有效带宽等的含义,以及「占用率是手段而非目的」 |
| 理解 延迟隐藏与占用率下界 的定性关系 | 能复述「足够活跃线程束以覆盖内存延迟」的思路,以及为何占用率过低往往需增大块或网格 |
| 掌握 主机端错误检查:CHECK 宏与同步捕获核函数错误 | 能写出 CHECK 宏并说明为何核函数错误需在 cudaDeviceSynchronize 或 cudaMemcpy 后检查 |
| 掌握 设备端调试:cuda-memcheck、printf、单线程 | 能说明 cuda-memcheck 的用法与局限;何时用 printf、何时用 <<<1,1>>> 做数值对比 |
| 掌握 从 C 到 CUDA 移植 的步骤与常见陷阱 | 能列出「仅用单块、忽略错误、未做边界检查、主机/设备指针混用、同步理解错误」及对应做法 |
| 会做 正确性验证:小数据、单块/单线程、与 CPU 对比 | 能写出「先正确再优化」的验证流程与简单回归对比 |
1.3 博客阅读导图(本章架构)
1 | 第 10 章 程序实现的注意事项 |
二、CUDA C 开发流程(书 10.1 节)
本节对应书中对 CUDA C 程序从无到有的完整开发流程的介绍。需要留意的是:规范的流程能减少返工、便于团队协作,并确保性能调优有的放矢。
2.1 从需求到并行化策略
开发任何 CUDA 程序的第一步都是明确需求:要解决什么问题、数据规模多大、精度与性能目标如何。接下来需要判断哪些部分适合放在 GPU 上。通常只有计算密集型、数据并行的代码段才值得移植到 GPU;若某段代码在 CPU 上仅占 5% 的运行时间,即使其在 GPU 上加速 10 倍,整体加速比也有限(阿姆达尔定律)。因此,先做热点分析(用 CPU 剖析工具或经验判断),把时间花在「真正占大头的循环或函数」上,是开发流程中的关键一步。
在确定要移植的热点之后,需要选择并行化策略。与第 1 章、第 2 章呼应,常见策略包括:(1) 使用现成 GPU 库(如 cuBLAS、cuFFT、cuSPARSE),在满足接口与精度要求的前提下优先考虑;(2) 手写核函数,当问题高度定制化或库无法直接覆盖时采用。书中建议:能库则库,不能库再手写;手写时要从数据划分和线程组织(网格、块、线程索引)出发,确保逻辑正确再谈优化。
下表归纳「开发阶段」与典型产出,便于在项目中对照执行:
| 阶段 | 主要活动 | 典型产出 |
|---|---|---|
| 需求与热点 | 明确目标、CPU 热点分析 | 热点函数/循环列表、性能基线 |
| 并行化策略 | 选库或手写、数据与线程划分设计 | 核函数接口与执行配置草案 |
| 实现与验证 | 编写主机/设备代码、单元测试 | 正确性验证(与 CPU 或小数据对比) |
| 性能调优 | 剖析、定位瓶颈、迭代优化 | 优化后内核与性能报告 |
| 集成与部署 | 与现有系统集成、发布 | 可交付程序与文档 |
2.2 APOD 与迭代优化 ★ 重难点
一种常见的 GPU 应用开发与优化循环是 APOD(Assess, Parallelize, Optimize, Deploy)。其含义为:
- Assess(评估):用剖析工具找出应用中耗时最多的部分,确定并行化候选;建立性能基线(如当前 CPU 运行时间)。
- Parallelize(并行化):对候选部分进行 GPU 移植(库或手写核函数),先保证正确性,再考虑性能。
- Optimize(优化):基于剖析结果迭代优化——内存访问、执行配置、占用率、指令级等(对应第 3~7 章内容)。
- Deploy(部署):将优化后的版本集成并发布;之后可以重新 Assess,开始下一轮迭代。
这一循环强调数据驱动:用剖析数据决定「优化哪里」,而不是凭感觉。在资源有限的情况下,应集中优化对整体性能影响最大的内核或传输,避免在次要路径上过度优化。
理解与体会:APOD 把「先正确再快」和「用数据说话」固化成了流程。很多人容易犯「一上来就调 block 大小、改访存」的毛病,结果要么数值不对查半天,要么在无关紧要的内核上花时间。养成「先 Assess 再动手、每次只改一类因素并记录」的习惯,能显著提高调优效率。
2.3 正确性优先与测试
在开发流程中,正确性优先于性能。需要留意的是:先写出能得到与 CPU 或已知结果一致的 GPU 实现,再在此基础上做性能调优。典型做法包括:(1) 小规模数据 + 单块单线程(如 <<<1, 1>>>)做数值对比,排除并行化带来的逻辑错误;(2) 中间结果写回主机并用 CPU 校验(如逐元素比较);(3) 单元测试:对核函数或关键步骤写小型测试用例,便于回归。
下面给出一个与书中风格一致的、带 CHECK 宏与简单验证的 CUDA 程序骨架,对应「实现与验证」阶段的主流程:
1 |
|
编译与运行:nvcc -o app app.cu && ./app。书中 2.1.7 节已说明:核函数启动是异步的,因此必须在核函数完成后同步(如 cudaDeviceSynchronize() 或一次 D2H 的 cudaMemcpy)再检查返回值,才能捕获核函数内部的错误。
三、剖析驱动的性能调优(书 10.2 节)
本节对应书中对性能剖析与基于剖析结果进行优化的讲解。没有剖析就优化,往往事倍功半;剖析能告诉你「时间花在哪里、是计算受限还是内存受限」,从而有针对性地应用前面各章的优化手段。
3.1 剖析工具与关键指标
CUDA 生态中常用的剖析工具包括 nvprof(命令行)、NVIDIA Nsight Systems(时间线、CPU/GPU 活动)、NVIDIA Nsight Compute(内核级详细指标)。本节要点:用 nvprof 可快速得到内核耗时与大致瓶颈(如 nvprof ./myapp);若需更细的指标(如每内核的占用率、内存吞吐、warp 效率),则使用 Nsight Compute。无论使用哪种工具,都应先确保程序正确,再在有代表性的数据规模下进行剖析,避免小规模或异常路径主导结论。
下表归纳书中或 CUDA 文档中常见的性能指标及其含义,便于在剖析报告中解读:
| 指标类型 | 典型指标 | 含义与用途 |
|---|---|---|
| 时间 | 内核/传输耗时 | 定位最耗时的内核或 H2D/D2H 传输 |
| 占用率 | achieved_occupancy | 实际活跃线程束数/理论最大,评估延迟隐藏 |
| 内存 | gld_efficiency 等 | 全局加载/存储效率,与合并访问、对齐相关 |
| 计算 | 吞吐(GFlops) | 判断计算受限 vs 内存受限 |
| 分支 | warp_execution_efficiency | 受线程束分化影响,越低越需优化控制流 |
需要留意的是:占用率是手段而非目的——足够的占用率用于隐藏延迟,但盲目追求 100% 占用率可能导致寄存器溢出或共享内存不足,反而降低性能。优化时应结合「瓶颈在哪」:若内存带宽已饱和,再提高占用率对带宽受限内核帮助有限;若瓶颈在计算,则需从指令数、分支、循环展开等方面入手(第 7 章)。
使用 nvprof 进行快速剖析的典型命令如下(与书中及第 2 章用法一致):
1 | nvcc -o myapp myapp.cu |
若需更细的内核级指标,可使用 nvprof --metrics achieved_occupancy,gld_efficiency ./myapp 等。NVIDIA 推荐在新环境中使用 Nsight Systems(nsys profile ./myapp)与 Nsight Compute 做更完整的分析;各工具有其适用场景与输出解读,可参考 Nsight 文档。
3.2 延迟隐藏与占用率的下界 ★ 易错点
第 3 章已给出「为隐藏延迟所需的最小线程数」的概念。要隐藏全局内存访问延迟(约 400~800 周期),每个 SM 上需要有足够多的活跃线程束,使得在等待内存的周期内,其他线程束可以执行。一个常用的下界估计是:每个 SM 上至少需要能「覆盖延迟 × 带宽」的并发度。从线程束数量角度,可写成:
[
\text{所需活跃线程束数} \gtrsim \frac{\text{内存延迟(周期)}}{\text{每线程束完成一次内存请求所需周期}}
]
例如,若内存延迟约 400 周期,而每个线程束平均每 32 周期能发出一批内存请求,则大约需要 (400/32 \approx 12) 个以上的活跃线程束才能较好地隐藏延迟。原书表 3-3 等会给出更具体的数值;此处强调:剖析得到的 achieved_occupancy 若过低(如远低于 0.25),往往意味着块太小或每块线程数过少,可尝试增大块大小或网格大小(在资源限制内)以提升占用率,从而更好地隐藏延迟。
难点与学习思考:不要孤立地「冲高占用率」——若内核已经受限于内存带宽,再增加块内线程数可能导致更多寄存器/共享内存使用,反而降低每 SM 可容纳的块数或引发溢出。第 3 章矩阵求和实验已经说明:最佳配置不一定是占用率最高的配置,要与 gld_efficiency、实际带宽一起看,做平衡。
3.3 迭代优化流程与有效带宽公式
典型的迭代优化流程为:(1) 用 nvprof 或 Nsight 找出耗时最高的内核;(2) 查看该内核的 achieved_occupancy、内存效率、计算吞吐;(3) 若内存效率低,则检查访问模式(第 4 章:合并、对齐、bank 冲突);若占用率低,则检查块大小与资源使用(寄存器、共享内存);(4) 修改代码后重新编译、运行剖析、对比前后指标。关键原则:每次只改一类因素,便于归因;并记录每次修改前后的数据,形成可复现的调优记录。
有效带宽(Effective Bandwidth) 是评估内存相关优化效果的常用公式,第 2 章、第 4 章均有涉及。对一次内核执行而言:
[
\text{有效带宽(GB/s)} = \frac{(\text{读字节数} + \text{写字节数})}{\text{运行时间(秒)} \times 10^9}
]
单位换算后,若内核的「读+写」总量为 (B) 字节,运行时间为 (T) 秒,则有效带宽为 (B/(T \times 10^9)) GB/s。与设备峰值带宽对比,可得到「带宽利用率」;若接近峰值,说明该内核很可能是内存受限,进一步优化应侧重减少传输量或提高合并度,而非单纯增加计算。
四、CUDA 程序的调试(书 10.3 节)
本节对应书中对 CUDA 程序调试方法 的介绍。CUDA 的异步执行与主机/设备分离,会使错误表现得不直观;掌握系统化的调试手段能显著提高排错效率。
4.1 主机端错误与 API 检查
所有 CUDA 运行时 API(如 cudaMalloc、cudaMemcpy、核函数启动的同步包装)都会返回 cudaError_t。不检查返回值是实践中最常见的错误来源之一:例如设备内存不足时 cudaMalloc 返回 cudaErrorMemoryAllocation,若忽略则后续可能出现非法访问或难以理解的崩溃。需要留意的是:每个 API 调用都应用错误处理宏包裹(如前文及第 2 章的 CHECK 宏),在调试版本中至少打印出错位置与 cudaGetErrorString(err)。
核函数启动是异步的,因此内核内部的错误(如非法内存访问、断言失败)不会在 kernel<<<...>>>() 返回时立即反映到主机;主机得到的 cudaSuccess 只表示「启动请求已提交」。要捕获核函数错误,必须在核函数完成后同步一次,例如 cudaDeviceSynchronize() 或 cudaMemcpy(..., cudaMemcpyDeviceToHost),再检查返回值。书中建议:在开发与调试阶段,在关键步骤后加上 CHECK(cudaDeviceSynchronize()),便于尽早发现设备端错误。
下表归纳书中提到的主机端常见错误类型与应对:
| 错误类型 | 典型原因 | 应对建议 |
|---|---|---|
| 内存分配失败 | 设备内存不足、碎片 | 检查分配大小、减少峰值占用或分批 |
| 无效参数 | 指针为 NULL、size 为 0 | 检查传入参数与前置逻辑 |
| 未初始化/设备不可用 | 驱动与运行时未正确初始化 | 检查环境、驱动版本、当前设备 |
| 核函数错误 | 非法访问、断言、未同步 | 同步后检查、使用 cuda-memcheck |
理解与体会:很多人第一次写 CUDA 时会在「为什么 cudaMalloc 成功了但后面崩了」上栽跟头——其实是更早的某次 API 已经失败却没检查。养成「所有 CUDA API 都用 CHECK 包一层、核函数后加一次同步再查错」的习惯,能省下大量无谓的排查时间。
4.2 设备端错误与 cuda-memcheck
设备端错误(如越界访问、未对齐访问)往往表现为「某次运行崩溃」或「结果偶尔错误」,难以仅靠主机端 API 返回值定位。cuda-memcheck(或 CUDA 工具包中的 memcheck 工具)用法:在程序前加前缀运行,例如:
1 | cuda-memcheck ./myapp |
工具会报告非法内存访问、未对齐访问等问题的近似位置(如某核函数、某块某线程),便于缩小排查范围。打开 memcheck 时程序会变慢,因此适合在调试阶段、小数据或单次运行下使用,不适合做大规模性能测试。
此外,核函数内 printf(Fermi 及以后架构支持)和单线程执行(<<<1, 1>>>)是两种简单实用的调试手段:printf 可输出线程索引与关键变量,单线程执行可将核函数退化为串行,便于与 CPU 结果逐点对比。printf 会严重影响性能且可能改变执行顺序,仅用于调试,发布前应移除或条件编译。
4.3 图形化调试器与 Nsight
对于需要单步、断点、查看变量的应用,NVIDIA Nsight 系列(如 Nsight Visual Studio Edition、Nsight Systems、Nsight Compute)的调试能力:在主机端和设备端设断点、查看设备内存与变量、与时间线结合分析。使用图形化调试器时,通常需要以调试模式编译(如 -G 生成设备调试信息),并注意多线程/多块下的断点行为(例如条件断点只对特定 blockIdx/threadIdx 生效)。需要留意的是:调试版本不应用于性能评估,优化时要用 Release 构建并配合剖析工具。
五、从 C 到 CUDA 的移植:案例与最佳实践(书 10.4 节)
本节对应书中对将现有 C(或 C++)程序移植到 CUDA 的流程、常见陷阱与案例的讲解。移植不是简单地把循环改成核函数,而是要识别数据并行、设计线程划分、处理数据依赖与边界,并避免一些典型错误。
5.1 识别热点与可并行性
移植的第一步是找到值得移植的热点。通常用 CPU 剖析工具(如 gprof、VTune、Visual Studio Profiler)找出占用时间最多的函数或循环;若某段代码占总时间比例很小,移植到 GPU 的收益有限。其次,分析该段代码的数据相关性:若循环迭代之间相互独立(仅读共享输入、写独立输出),则适合做数据并行;若存在严重的跨迭代依赖(如紧耦合的递推),可能需要算法级改造或仅对部分阶段做 GPU 加速。
数据划分必须与线程组织一致。例如,对一维数组的逐元素操作,常用全局索引 (i = \text{blockIdx.x} \times \text{blockDim.x} + \text{threadIdx.x}),并在线程内做边界判断 if (i < n)。对二维问题,可扩展为二维网格与块,或在一维索引下用 i = row \times \text{width} + col 计算行列。与第 2 章一致:先保证「每个数据元素由哪个线程处理」定义清晰且无遗漏、无重复,再考虑优化执行配置与内存访问。
5.2 常见移植陷阱 ★ 易错点
常见移植陷阱归纳如下表,便于对照避免:
| 陷阱 | 表现与原因 | 建议做法 |
|---|---|---|
| 仅用单块 | 无法利用多 SM,GPU 利用率极低 | 用网格覆盖全部数据,块数 ≥ SM 数倍数 |
| 忽略错误检查 | 分配失败或内核错误被忽略,难以定位 | 所有 API 用 CHECK 等宏包裹,调试时加同步检查 |
| 未做边界检查 | 数据规模不能被块大小整除时越界 | 核函数内使用 if (i < n) 等保护 |
| 主机/设备指针混用 | 在设备代码中使用主机指针或反之 | 严格区分 h_/d_ 命名与拷贝时机 |
| 同步理解错误 | 以为核函数返回即执行完毕 | 需要结果时用 cudaMemcpy 或 cudaDeviceSynchronize |
书中还会提到:首次移植应先追求正确性——例如先用小数据、单块或少量块验证数值,再逐步增大规模与块数;同时保留一份 CPU 参考实现,便于回归对比。
难点与学习思考:「仅用单块」和「忽略错误检查」是初学者最容易犯的两个致命错误。前者会导致 GPU 只动用一块 SM,性能甚至可能不如 CPU;后者会让 cudaMalloc 失败或内核越界等问题被掩盖,直到在别的代码路径或数据规模下才暴露,排查成本很高。移植时务必:网格覆盖全数据、核函数内边界判断、所有 API 检查、核函数后同步查错。
5.3 移植案例:标量循环到核函数
下面给出一个与书中风格一致的简单移植案例:将 C 中的标量循环「数组每元素乘 2 并加 1」改为 CUDA 核函数,并保持与 CPU 结果一致。对应「从 C 到 CUDA」的典型步骤:识别独立循环 → 用线程索引替代循环变量 → 设备分配与拷贝 → 启动核函数 → 拷回并验证。
原始 C 循环(书中风格):
1 | for (int i = 0; i < n; i++) |
对应的 CUDA 核函数与主机调用:
1 | __global__ void scaleAndShift(const float *in, float *out, int n) { |
这里「每个 i 对应一个线程」且彼此无依赖,是典型的数据并行。执行配置使用常见的 256 线程/块,网格大小按 (n) 向上取整,核函数内用 if (i < n) 做边界保护。在真实项目中,移植后要用相同输入在 CPU 与 GPU 上各算一遍,逐元素或统计误差比较,确认无误后再做性能调优(如调整 blockSize、检查合并访问等)。
5.4 最佳实践小结
下表可作为「程序实现的注意事项」的速查表:
| 类别 | 最佳实践要点 |
|---|---|
| 开发流程 | 先热点分析再并行化;能库则库;正确性优先,再迭代优化(APOD)。 |
| 错误处理 | 所有 CUDA API 用宏检查返回值;调试时在核函数后同步以捕获设备端错误。 |
| 剖析与调优 | 用 nvprof/Nsight 定位瓶颈;关注占用率、内存效率与计算吞吐;每次改一类因素。 |
| 移植 | 识别独立循环与数据划分;核函数内做边界检查;保留 CPU 参考实现做回归。 |
| 资源与配置 | 网格/块覆盖全部数据,避免仅单块;在寄存器/共享内存限制内选择块大小。 |
六、本章小结与重难点回顾
本章系统梳理了程序实现的注意事项,将前几章的知识落实到工程流程、剖析、调试与移植中。下面用表格与要点形式做小结,便于复习与自测。
6.1 知识小结(与书中对应)
| 主题 | 要点 |
|---|---|
| 开发流程 | 需求与热点分析→并行化策略(库优先)→实现与验证→性能调优→集成部署;APOD。 |
| 剖析驱动调优 | 用 nvprof/Nsight 找瓶颈;占用率、内存效率、有效带宽;单因子改动、记录前后数据。 |
| 调试 | 主机端:CHECK 宏、同步后查错;设备端:cuda-memcheck、printf、单线程;Nsight。 |
| C 到 CUDA 移植 | 识别热点与可并行性;数据与线程划分一致;避免单块、忽略错误、越界;先正确再优化。 |
6.2 重难点速查
| 重难点 | 要点 |
|---|---|
| APOD | Assess→Parallelize→Optimize→Deploy;用剖析数据驱动优化对象;先正确再快。 |
| 占用率 | 手段而非目的;足够即可隐藏延迟,盲目追求可能溢出;与内存/计算瓶颈一起看。 |
| 核函数错误捕获 | 启动异步,必须 cudaDeviceSynchronize 或 D2H cudaMemcpy 后再检查 cudaGetLastError。 |
| 移植陷阱 | 仅用单块、忽略错误、未做边界检查、主机/设备指针混用、误解同步;对应做法见 5.2 节表。 |
| 有效带宽 | (读字节+写字节)/(时间×10^9) GB/s;与峰值对比判断内存/计算受限;指导优化方向。 |
6.3 学习思考
- 与第 2 章的衔接:第 2 章给出了 CHECK 宏、核函数后同步查错、nvprof 与有效带宽的初次使用;本章把这些纳入「开发流程」和「调试」规范,并强调正确性优先与 APOD 循环,形成从入门到工程的闭环。
- 与第 3~7 章的关系:剖析得到的占用率、内存效率、分支效率等,直接对应第 3 章(执行模型、延迟隐藏)、第 4 章(合并访问、带宽)、第 5 章(共享内存、bank 冲突)、第 7 章(指令级优化);本章强调「用剖析驱动」而不是盲目调参,每次只改一类因素便于归因。
- 实践建议:(1) 新项目一开始就养成「所有 API 用 CHECK、关键路径后同步查错」的习惯;(2) 移植时先小数据+单块或 <<<1,1>>> 验证数值,再放大规模;(3) 调优前先跑 nvprof 或 Nsight 确定瓶颈再动手,并记录每次改动前后的指标;(4) 保留 CPU 参考实现与单元测试,便于回归。
系列完结与后续方向
本系列博客共 10 章,对应《CUDA C编程权威指南》全书。第 10 章为最后一章;至此,从异构并行概念到多 GPU 与集群、再到程序实现与移植的完整学习路径已全部覆盖。
若希望继续深入,可参考以下方向:
- 进阶主题:更复杂的多流与多 GPU 负载均衡(第 6、9 章)、CUDA 与 MPI 的深度结合、领域库(如 cuDNN、TensorRT)在具体应用中的使用。
- 官方资源:NVIDIA CUDA 编程指南、Best Practices Guide、各库文档与样例代码。
- 实践建议:在实际项目中重复「剖析→优化→部署」的循环,并积累自己的移植与调优案例。
从第一章的「为什么需要 GPU」到本章的「如何实现与交付 CUDA 程序」——感谢你完成本系列的学习,祝你在 GPU 编程之路上越走越稳。
本章自测
- APOD 各字母代表什么?为何强调「用剖析驱动优化」?
- 为什么核函数内部出错时,主机端在 kernel 返回后立刻检查 cudaGetLastError 可能仍得到 cudaSuccess?应如何正确捕获?
- 从 C 移植到 CUDA 时,常见陷阱有哪些?至少写出三条。
答案与解析
- Assess(评估)、Parallelize(并行化)、Optimize(优化)、Deploy(部署)。用剖析驱动可避免盲目调参,先看时间花在哪里、是内存还是计算受限,再针对性应用第 3~7 章的优化手段,每次只改一类因素便于归因。
- 核函数启动是异步的,主机在启动后立即返回,此时设备可能尚未执行或尚未报错;核函数内的错误要等设备执行到该处才会发生。正确做法:在 kernel 后调用
cudaDeviceSynchronize()或一次 D2H 的cudaMemcpy,再检查返回值,才能捕获设备端错误。 - 常见陷阱包括:仅用单块导致规模受限、忽略 API 返回值导致难以定位崩溃、未做边界检查导致越界、主机/设备指针混用、误以为 kernel 返回即执行完毕而不同步就使用结果、未做正确性验证就优化等。
系列导航:导读 | 上一篇:第9章 多GPU编程
本文为「CUDA C编程权威指南」系列博客第 10 篇,共 10 章。基于《Professional CUDA C Programming》by John Cheng, Max Grossman, Ty McKercher。
