CUDA 编程基础
前言:为什么理解 CUDA 对检测管线至关重要
当我们在上一节中讨论"数据准备层"和"推理层"时,已经提到了 CUDA 加速 这个关键词。在 Autoware CenterPoint 的实现中,点云预处理(范围裁剪、多帧融合、坐标变换)、特征编码(VFE 的体素化操作)、甚至某些后处理流程(如范围过滤)都有 CUDA 实现的版本。这不是可选项,而是在边缘设备(如 NVIDIA AGX Orin)上达到实时性能的必要条件。
从性能角度,CUDA 的价值在于:
- 数据并行:激光雷达点云通常有 10 万到 100 万个点。CPU 串行处理需要数百毫秒,而 CUDA 可以用数十万个线程并行处理,延迟降至 5-20 ms。
- 内存带宽 :GPU 的内存带宽(如 3090 的 576 GB/s)远高于 CPU(约 50 GB/s),特别是在点云预处理这种高吞吐、低计算密度的操作中优势明显。
- 功耗效率:在有限的车载 GPU 功率预算下,CUDA 相比 CPU 的单位功耗性能比(Performance per Watt)高出 5-10 倍。
但从工程角度,理解 CUDA 更深层的价值在于:掌握 GPU 的执行模型与内存层次,能帮助我们快速定位性能瓶颈、评估优化空间、甚至自己设计更高效的算子。
这系列将通过逐层深化的讲解------从最核心的 Grid/Block/Thread 模型 ,到 内存层次与缓存机制 ,再到 CPU-GPU 交互的实战细节 ,最后到 ARM 异构架构的特殊考量------帮助你建立对 CUDA 的"第一性原理"级理解。本节,我们将专注于理解CUDA执行模型的基本概念。
第一部分:CUDA 执行模型------Grid、Block、Thread、Warp
为什么需要这样的层级结构?
传统 CPU 编程中,我们关心的是"进程→线程"的二层结构。而 CUDA 引入了 三层结构:Grid → Block → Thread,这是硬件现实的映射。
理解这个结构的关键是:GPU 不是一个单一的计算器,而是一个包含数十到数百个"小 CPU"(Streaming Multiprocessor, SM)的分布式系统。每个 SM 可以独立执行线程块,而 Block 内的线程可以在 SM 内共享资源(共享内存、寄存器等)。
Grid、Block、Thread 的定义与映射
┌─────────────────────────── GPU ────────────────────────────┐
│ │
│ ┌──────────────────────────── Grid ─────────────────────┐ │
│ │ (逻辑组织,定义有多少个Block需要执行) │ │
│ │ │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ Block (0,0) │ │ Block (1,0) │ ... │ │
│ │ │ │ │ │ │ │
│ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │
│ │ │ │ Thread 0~31 │ │ │ │ Thread 0~31 │ │ │ │
│ │ │ │ (Warp 0) │ │ │ │ (Warp 0) │ │ │ │
│ │ │ ├──────────────┤ │ │ ├──────────────┤ │ │ │
│ │ │ │ Thread 32~63 │ │ │ │ Thread 32~63 │ │ │ │
│ │ │ │ (Warp 1) │ │ │ │ (Warp 1) │ │ │ │
│ │ │ └──────────────┘ │ │ └──────────────┘ │ │ │
│ │ └──────────────────┘ └──────────────────┘ │ │
│ │ (在某个SM上执行) (在另一个SM上执行) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
关键术语定义
| 术语 | 含义 | 硬件对应 | 作用 |
|---|---|---|---|
| Grid | 任务的逻辑划分 | 所有 SM 的集合 | 定义有多少个 Block 需要执行 |
| Block | 一组合作线程 | 一个 SM(或 SM 的一部分) | 线程可以共享内存、同步 |
| Thread | 最小执行单位 | 一个标量执行器 | 执行具体的计算逻辑 |
| Warp | 32 个线程 | GPU 的硬件调度单位 | Warp 内的线程必须以 SIMT 方式同步执行 |
线程索引与全局 ID 计算
在 Kernel 函数内,我们需要通过 CUDA 提供的 内置变量 来定位当前线程:
cuda
// 内置变量(只能在kernel中访问)
blockIdx.x / blockIdx.y / blockIdx.z // 当前Block在Grid中的索引
blockDim.x / blockDim.y / blockDim.z // 每个Block包含的线程数
threadIdx.x / threadIdx.y / threadIdx.z // 当前Thread在Block内的索引
gridDim.x / gridDim.y / gridDim.z // Grid中Block的总数
实战例子:点云处理中的 1D 线程映射
假设我们要处理 1,000,000 个点,配置 Grid 和 Block 为:
cpp
int num_points = 1000000;
int threads_per_block = 256;
int num_blocks = (num_points + threads_per_block - 1) / threads_per_block; // 3907
dim3 grid(num_blocks); // (3907)
dim3 block(threads_per_block); // (256)
启动后,GPU 会创建 3,907 × 256 = 1,000,192 个线程(最后一个 Block 会有冗余线程)。在 Kernel 中计算全局索引:
cuda
__global__ void processPoints(float* points, int num_points) {
// 全局线程ID(1D 映射)
int point_id = blockIdx.x * blockDim.x + threadIdx.x;
// 边界检查:最后一个Block有冗余线程
if (point_id >= num_points) return;
// 处理第 point_id 个点
points[point_id * 3 + 0] += 1.0; // x
points[point_id * 3 + 1] += 1.0; // y
points[point_id * 3 + 2] += 1.0; // z
}
为什么选择 256 个线程/Block?
- Warp 大小:GPU 的硬件调度单位是 Warp(32 个线程)。256 = 32 × 8,正好是 32 的倍数,充分利用硬件。
- 占用率(Occupancy):每个 SM 可以并发执行多个 Block(通常 8-16 个)。太小的 Block(如 32)会导致 SM 资源浪费;太大的 Block(如 1024)会限制并发 Block 数。
- 共享内存限制 :每个 SM 的共享内存容量固定(如 96 KB)。Block 内的线程共享这块内存,太多线程会超过容量。256 通常是 最大吞吐 与 灵活配置 的最佳折衷。
从代码示例看线程执行
回到 generateSweepPoints_kernel 代码,这个 Kernel 的核心逻辑其实非常简单------每个线程独立处理一个点:
cuda
__global__ void generateSweepPoints_kernel(
const float * input_points, size_t points_size, int input_point_step, float time_lag,
const float * transform_array, int num_features, float * output_points)
{
// 全局线程ID
int point_idx = blockIdx.x * blockDim.x + threadIdx.x;
if (point_idx >= points_size) return;
// 每个线程独立地:
// 1. 读取自己的输入点坐标
const float input_x = input_points[point_idx * input_point_step + 0];
const float input_y = input_points[point_idx * input_point_step + 1];
const float input_z = input_points[point_idx * input_point_step + 2];
// 2. 执行 4x4 矩阵变换(没有线程间通信)
output_points[point_idx * num_features + 0] =
transform_array[0] * input_x + ... ;
// 3. 写入输出
}
这里体现了 GPU 编程的核心哲学:尽可能让每个线程独立工作,避免线程间的通信和同步。因为每条 Warp 的同步指令都可能导致整个 Warp 的停滞(Warp Divergence)。
经过这一部分的介绍,我们可以把 CUDA 的"并行执行语义"建立成直觉:GPU 不是把一段代码跑得更快,而是把同一段代码同时让海量线程去处理海量数据,因此必须先理解 Grid→Block→Thread 的组织方式,以及硬件真正调度的最小单位 Warp(32 线程)意味着什么。 结合点云场景,可以把"一个点对应一个线程"当作最常见的映射模板:用全局线程 ID 计算出 point_idx,每个线程各自读输入点、做坐标变换、写输出,并通过边界判断处理尾部冗余线程,从而形成稳定、可扩展、易验证的数据并行骨架。 同时,线程块大小(例如 256)之所以常用,本质是在 Warp 对齐、并发占用率、资源约束(寄存器/共享内存)之间做工程折中,这个折中会直接决定同一份 kernel 在不同平台上的吞吐表现。
有了这套执行模型的"地图"之后,下一节我们将介绍第二部分:当线程映射方式确定,性能差异往往不再取决于计算本身,而取决于内存怎么读写------数据放在哪里(寄存器/共享内存/全局内存/常量内存)、访问是否连续合并、是否命中缓存、是否需要同步与原子操作,才是点云预处理这类高带宽任务的真正瓶颈来源。