1. 课程导入与GPU图像处理全景
图像处理是典型的“数据量大、计算规则统一、局部性强”的任务。以一张1920×1080的RGB图像为例,它包含约622万个像素、1866万个字节的原始数据。若对每个像素做一次3×3卷积,需要约9次乘加运算,整图就是约5600万次乘加。CPU单核在串行执行时,不仅要逐像素循环,还要反复访问内存,缓存命中率低,实际耗时往往远超理论算力。这就是我们常说的“CPU串行瓶颈”:算力不是不够,而是数据搬运和指令调度把时间吃掉了。
CUDA的核心思维不是“让一个核跑得更快”,而是“让成千上万个轻量线程同时处理不同数据”。GPU拥有大量流处理器,适合SIMT(单指令多线程)模式:同一段代码,不同线程处理不同像素。图像处理天然满足这一条件——每个输出像素的计算只依赖固定邻域输入,线程之间几乎不需要通信。因此,把图像任务映射到GPU,通常能获得数十倍甚至上百倍的加速。
本课程的目标是让你从“会写CUDA”走到“会优化CUDA图像算法”。具体包括三件事:第一,理解GPU的线程层次、内存层次与执行模型;第二,掌握用CUDA实现常见图像算子(灰度化、卷积、滤波、边缘检测、直方图等);第三,学会用工具定位瓶颈并做针对性优化,而不是盲目调参。
工具链:nvcc、Nsight与OpenCV CUDA模块
课程使用的工具链分为三层。最底层是nvcc,它负责把.cu文件编译成GPU可执行的PTX/SASS,并链接CUDA运行时。你需要熟悉几个关键编译选项:-arch=sm_75指定目标架构,-O3开启优化,--ptxas-options=-v查看寄存器与共享内存使用量。寄存器溢出和共享内存超限是新手最常见的性能杀手,这个选项能直接暴露问题。
中间层是Nsight系列工具。Nsight Systems用于系统级时间线分析,能看清CPU与GPU的 overlap、内核启动间隔、内存拷贝占比;Nsight Compute用于单核深度剖析,能给出SM占用率、内存吞吐、warp stall原因。排障时先看Systems判断“是不是拷贝拖慢了”,再用Compute判断“核内是不是访存不合并”。
上层是OpenCV CUDA模块。它提供了cv::cuda::GpuMat和大量已优化算子,如cv::cuda::cvtColor、cv::cuda::filter2D、cv::cuda::Canny。课程不会只教你调库,而是会对照OpenCV的实现思路,手写关键核函数,理解它为什么快。这样你既能快速交付,也能在库不满足需求时自己改。
实战项目路线:从灰度化到完整优化闭环
课程以一个贯穿始终的实战项目为主线:对一组1920×1080的RGB图像做“灰度化→高斯滤波→Sobel边缘检测→直方图统计”。这条链路覆盖了图像处理中最典型的四类操作:逐元素变换、邻域卷积、梯度计算、全局归约。每一类都有不同的优化重点。
灰度化是逐元素操作,优化重点是合并访存。RGB三通道连续存储时,一个线程读三个字节,若线程编号与像素编号对齐,相邻线程访问的地址是连续的,能触发128字节事务。示例中,我们让每个线程处理一个像素,用uchar3读取,计算0.299R+0.587G+0.114B,再写入单通道输出。这个核函数简单,但能让你第一次直观看到“合并访问”对带宽的影响。
高斯滤波是邻域操作,优化重点是共享内存与边界处理。3×3卷积中,每个输出像素需要9个输入。若直接读全局内存,每个输入像素会被重复读取9次。我们让每个线程块先把输入块加载到共享内存,再从共享内存取邻域。示例参数:块大小16×16,共享内存数组18×18(含1像素边界),边界线程负责加载额外行/列。这样全局内存读取量从9倍降到约1.27倍。
Sobel边缘检测在卷积基础上增加梯度幅值计算,优化重点是寄存器复用与指令调度。x方向和y方向的两个卷积核可以共享同一组邻域数据,先加载到寄存器,再分别做乘加。这样避免重复访存,也减少共享内存压力。
直方图统计是全局归约类操作,优化重点是原子操作与私有化。256个bin的直方图,若所有线程直接对全局内存做atomicAdd,冲突严重。我们让每个线程块在共享内存中维护私有直方图,块内用atomicAdd累加,最后再合并到全局。示例中,块大小256,共享内存直方图256×4字节=1KB,合并阶段每个线程负责一个bin。
cudaEvent记录核函数耗时,不要用CPU计时器包住整个流程,否则会把拷贝时间算进去。
如何判断优化是否有效
优化不是“感觉快了”,而是有明确指标。课程中我们关注四个量:核函数耗时、有效内存带宽、SM占用率、warp stall原因。有效带宽=读写字节数/耗时,若远低于理论带宽(如RTX 3060约360GB/s),说明访存模式有问题。SM占用率低可能是寄存器或共享内存限制,也可能是块大小不合理。warp stall中long_scoreboard占比高,说明全局内存延迟没被隐藏,需要增加并行度或改用共享内存。
排障顺序建议:先用Nsight Systems看整体时间线,确认瓶颈在核函数还是拷贝;再用Nsight Compute看单核指标,定位是访存、计算还是同步问题;最后回到代码做针对性修改。不要一上来就改块大小或加__restrict__,那样容易掩盖真正的问题。