Autoware Universe 感知模块详解 | 第十二节 CUDA 编程基础——CUDA执行模型

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 在不同平台上的吞吐表现。

有了这套执行模型的"地图"之后,下一节我们将介绍第二部分:当线程映射方式确定,性能差异往往不再取决于计算本身,而取决于内存怎么读写------数据放在哪里(寄存器/共享内存/全局内存/常量内存)、访问是否连续合并、是否命中缓存、是否需要同步与原子操作,才是点云预处理这类高带宽任务的真正瓶颈来源。

相关推荐
Hi2024021717 小时前
如何通过选择正确的畸变模型解决相机标定难题
人工智能·数码相机·计算机视觉·自动驾驶
yuanmenghao1 天前
CAN系列 — (8) 为什么 Radar Object List 不适合“直接走 CAN 信号”
网络·数据结构·单片机·嵌入式硬件·自动驾驶·信息与通信
RockHopper20251 天前
驾驶认知的本质:人类模式 vs 端到端自动驾驶
人工智能·神经网络·机器学习·自动驾驶·具身认知
益莱储中国1 天前
2026 CES 聚焦 Physical AI:AI 硬件、具身智能、自动驾驶、芯片战争、机器人、显示技术等全面爆发
人工智能·机器人·自动驾驶
Hi202402171 天前
相机与激光雷达联合标定:如何选择高辨识度的参照物
数码相机·自动驾驶·雷达·相机标定·机器视觉
小烤箱2 天前
Autoware Universe 感知模块详解 | 第十一节:检测管线的通用工程模板与拆解思路导引
人工智能·机器人·自动驾驶·autoware·感知算法
纪伊路上盛名在2 天前
如何为我们的GPU设备选择合适的CUDA版本和Torch版本?
pytorch·深度学习·torch·cuda·英伟达
容智信息2 天前
Hyper Agent:企业级Agentic架构怎么实现?
人工智能·信息可视化·自然语言处理·架构·自动驾驶·智慧城市
退休钓鱼选手2 天前
BehaviorTree行为树-机器人及自动驾驶
人工智能·自动驾驶